import assert from 'node:assert/strict'; import { rm } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { after, before, test } from 'node:test'; import { createApp } from '../src/app.js'; import { loadConfig } from '../src/config.js'; import { createContentStore } from '../src/content.js'; import { createFixture } from './fixtures.js'; let dir; let server; let base; const sent = []; before(async () => { dir = await createFixture(); const config = loadConfig({ production: true, showDrafts: false, contentDir: dir, cacheDir: path.join(os.tmpdir(), `site-cache-${process.pid}`), siteUrl: 'https://example.org', }); const content = createContentStore(config); const mailer = { enabled: true, send: async (message) => sent.push(message) }; const app = await createApp({ config, content, mailer }); server = app.listen(0); await new Promise((resolve) => server.once('listening', resolve)); base = `http://127.0.0.1:${server.address().port}`; }); after(async () => { server.close(); await rm(dir, { recursive: true, force: true }); await rm(path.join(os.tmpdir(), `site-cache-${process.pid}`), { recursive: true, force: true }); }); const get = (url, options) => fetch(base + url, { redirect: 'manual', ...options }); test('pages render in both languages', async () => { for (const url of [ '/de/', '/en/', '/de/portfolio', '/en/portfolio', '/de/portfolio/alpha', '/en/portfolio/alpha', '/de/werdegang', '/en/journey', '/de/kontakt', '/en/contact', '/de/datenschutz', '/en/privacy', ]) { const res = await get(url); assert.equal(res.status, 200, url); assert.match(res.headers.get('content-security-policy'), /script-src 'self'/); } }); test('the root redirects by Accept-Language', async () => { let res = await get('/', { headers: { 'Accept-Language': 'en-GB,en;q=0.9' } }); assert.equal(res.status, 302); assert.equal(res.headers.get('location'), '/en/'); res = await get('/', { headers: { 'Accept-Language': 'fr' } }); assert.equal(res.headers.get('location'), '/de/'); }); test('drafts, hidden pages and wrong slugs are 404', async () => { for (const url of ['/de/portfolio/beta', '/de/homelab', '/de/journey', '/en/werdegang', '/de/nope', '/xx']) { const res = await get(url); assert.equal(res.status, 404, url); } }); test('homepage hides links to pages that do not exist yet', async () => { const html = await (await get('/de/')).text(); assert.doesNotMatch(html, /href="\/de\/homelab"/); assert.match(html, /

{ const html = await (await get('/de/portfolio?tag=nothing')).text(); assert.doesNotMatch(html, /data-tags="web" hidden/); }); test('media: resized images, no traversal, no source files', async () => { let res = await get('/media/projects/alpha/cover.png?w=480'); assert.equal(res.status, 200); assert.equal(res.headers.get('content-type'), 'image/webp'); res = await get('/media/projects/alpha/cover.png?w=480&v=abc'); assert.match(res.headers.get('cache-control'), /immutable/); res = await get('/media/projects/alpha/cover.png?w=123'); assert.equal(res.status, 404); for (const url of ['/media/site.yml', '/media/projects/alpha/de.md', '/media/..%2Fpackage.json', '/media/projects/%2e%2e/%2e%2e/site.yml']) { res = await get(url); assert.equal(res.status, 404, url); } }); test('sitemap and robots', async () => { const sitemap = await (await get('/sitemap.xml')).text(); assert.match(sitemap, /https:\/\/example\.org\/en\/portfolio\/alpha<\/loc>/); assert.doesNotMatch(sitemap, /beta|homelab/); assert.match(await (await get('/robots.txt')).text(), /Sitemap: https:\/\/example\.org\/sitemap\.xml/); }); const form = (fields) => new URLSearchParams({ name: 'Ada', email: 'ada@example.org', message: 'Hallo, ich habe eine Frage.', ts: String(Date.now() - 10_000), ...fields }); const post = (body, headers = {}) => get('/de/kontakt', { method: 'POST', body, headers: { Accept: 'application/json', ...headers } }); test('contact form', async (t) => { await t.test('sends valid messages', async () => { const res = await post(form()); assert.equal(res.status, 200); assert.equal((await res.json()).ok, true); assert.equal(sent.length, 1); assert.equal(sent[0].replyTo.address, 'ada@example.org'); }); await t.test('rejects invalid input with field errors', async () => { const res = await post(form({ email: 'nope', message: 'kurz' })); assert.equal(res.status, 422); const data = await res.json(); assert.ok(data.errors.email); assert.ok(data.errors.message); }); await t.test('quietly drops bots', async () => { let res = await post(form({ website: 'http://spam' })); assert.equal((await res.json()).ok, true); res = await post(form({ ts: String(Date.now()) })); assert.equal((await res.json()).ok, true); assert.equal(sent.length, 1); }); await t.test('blocks foreign origins', async () => { const res = await post(form(), { Origin: 'https://evil.example' }); assert.equal(res.status, 403); }); await t.test('rate limits', async () => { // 4 requests above counted, plus this one = 5, the next one is too many assert.equal((await post(form())).status, 200); const res = await post(form()); assert.equal(res.status, 429); assert.match((await res.json()).message, /Zu viele/); }); await t.test('works as a plain form post', async () => { const res = await get('/en/contact', { method: 'POST', body: form() }); assert.equal(res.status, 429); assert.match(await res.text(), /Too many messages/); }); });