162 lines
5.6 KiB
JavaScript
162 lines
5.6 KiB
JavaScript
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, /<h1 class="hero__title"/);
|
|
});
|
|
|
|
test('portfolio filter works without JavaScript', async () => {
|
|
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, /<loc>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/);
|
|
});
|
|
});
|