Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,17 @@ app.use('/panel', panelRoutes);
// Hot path is in-memory only; no DB/disk reads per request.
app.get('/', (req, res) => homepageService.respond(req, res));
app.head('/', (req, res) => homepageService.respond(req, res));
app.get([
'/assets/*',
'/apple-touch-icon.png',
'/favicon.ico',
'/favicon.svg',
'/favicon-96x96.png',
'/site.webmanifest',
'/vite.svg',
'/web-app-manifest-192x192.png',
'/web-app-manifest-512x512.png',
], (req, res, next) => homepageService.serveTemplateAsset(req, res, next));

// ==================== ERROR HANDLING ====================

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "node scripts/test-session-cookie-config.js && node scripts/test-i18n-fallback.js && node scripts/test-node-ui-meta.js && node scripts/test-node-active-api.js && node scripts/test-auth-subscription-boundaries.js",
"test": "node scripts/test-session-cookie-config.js && node scripts/test-i18n-fallback.js && node scripts/test-node-ui-meta.js && node scripts/test-node-active-api.js && node scripts/test-auth-subscription-boundaries.js && node scripts/test-homepage-templates.js",
"build:assets": "npx --yes esbuild public/css/style.css public/css/network.css --minify --loader:.css=css --outdir=public/css --out-extension:.css=.min.css --allow-overwrite && npx --yes esbuild public/js/network.js --minify --outfile=public/js/network.min.js"
},
"dependencies": {
Expand Down
138 changes: 138 additions & 0 deletions scripts/test-homepage-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const Module = require('module');

function createResponse() {
return {
statusCode: 200,
headers: {},
body: null,
ended: false,
setHeader(name, value) {
this.headers[name.toLowerCase()] = value;
},
removeHeader(name) {
delete this.headers[name.toLowerCase()];
},
status(code) {
this.statusCode = code;
return this;
},
send(value) {
this.body = value;
this.ended = true;
return this;
},
end() {
this.ended = true;
return this;
},
};
}

async function withHomepageService(settings, run) {
const originalLoad = Module._load;
const updates = [];

Module._load = function patchedLoad(request, parent, isMain) {
if (parent?.filename?.endsWith('/src/services/homepageService.js')) {
if (request === '../models/settingsModel') {
return {
get: async () => settings,
update: async (update) => {
updates.push(update);
return settings;
},
};
}
if (request === '../utils/logger') {
return { info() {}, warn() {}, error() {}, debug() {} };
}
}
return originalLoad.call(this, request, parent, isMain);
};

try {
delete require.cache[require.resolve('../src/services/homepageService')];
const homepageService = require('../src/services/homepageService');
return await run(homepageService, updates);
} finally {
Module._load = originalLoad;
delete require.cache[require.resolve('../src/services/homepageService')];
}
}

function writeTemplate(root, slug, html = '<!doctype html><h1>template</h1>') {
const dir = path.join(root, slug);
fs.mkdirSync(path.join(dir, 'assets'), { recursive: true });
fs.writeFileSync(path.join(dir, 'index.html'), html);
fs.writeFileSync(path.join(dir, 'assets', 'app.js'), 'console.log("asset");');
}

async function main() {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'celerity-sni-templates-'));
process.env.SNI_TEMPLATES_DIR = tempRoot;

try {
writeTemplate(tempRoot, 'alpha', '<!doctype html><h1>alpha masking</h1>');
writeTemplate(tempRoot, 'beta', '<!doctype html><h1>beta masking</h1>');
fs.mkdirSync(path.join(tempRoot, 'no-index'), { recursive: true });
writeTemplate(tempRoot, '.hidden', '<!doctype html><h1>hidden</h1>');

await withHomepageService({ homepage: { mode: 'nginx' } }, async (homepageService) => {
assert.deepStrictEqual(
homepageService.listTemplates().map(template => template.slug),
['alpha', 'beta'],
'only non-hidden template directories with index.html should be listed'
);
assert.strictEqual(homepageService.isValidTemplateSlug('alpha'), true);
assert.strictEqual(homepageService.isValidTemplateSlug('../alpha'), false);
assert.strictEqual(homepageService.isValidTemplateSlug('.hidden'), false);
assert.strictEqual(homepageService.isValidTemplateSlug('no-index'), false);
});

await withHomepageService(
{ homepage: { mode: 'template', templateSlug: 'alpha' } },
async (homepageService) => {
await homepageService.init();
const res = createResponse();
homepageService.respond({ method: 'GET', headers: {} }, res);

assert.strictEqual(homepageService.getMode(), 'template');
assert.strictEqual(homepageService.getTemplateSlug(), 'alpha');
assert.match(String(res.body), /alpha masking/);
assert.strictEqual(
homepageService.resolveTemplateAssetPath('/assets/app.js'),
path.join(tempRoot, 'alpha', 'assets', 'app.js')
);
assert.strictEqual(homepageService.resolveTemplateAssetPath('/assets/../index.html'), null);
assert.strictEqual(homepageService.resolveTemplateAssetPath('/panel'), null);
}
);

await withHomepageService(
{ homepage: { mode: 'template', templateSlug: 'missing' } },
async (homepageService, updates) => {
await homepageService.init();

assert.strictEqual(homepageService.getMode(), 'nginx');
assert.deepStrictEqual(updates[0], {
'homepage.mode': 'nginx',
'homepage.templateSlug': '',
});
}
);
} finally {
delete process.env.SNI_TEMPLATES_DIR;
fs.rmSync(tempRoot, { recursive: true, force: true });
}

console.log('homepage template tests passed');
}

main().catch((error) => {
console.error(error);
process.exit(1);
});
Binary file added sni-templates/10gag/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
120 changes: 120 additions & 0 deletions sni-templates/10gag/assets/script.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions sni-templates/10gag/assets/style.css

Large diffs are not rendered by default.

Binary file added sni-templates/10gag/favicon-96x96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added sni-templates/10gag/favicon.ico
Binary file not shown.
3 changes: 3 additions & 0 deletions sni-templates/10gag/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions sni-templates/10gag/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<title>Meme Flow - Endless Fun</title>
<meta name="description" content="Your daily dose of laughter with endless memes, GIFs, and viral content" />
<script type="module" crossorigin src="/assets/script.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
21 changes: 21 additions & 0 deletions sni-templates/10gag/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file added sni-templates/10gag/web-app-manifest-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added sni-templates/10gag/web-app-manifest-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading