import { strict as assert } from 'node:assert'; import path from 'node:path'; import { rm } from 'node:fs/promises'; import { afterEach, beforeEach, describe, it } from 'node:test'; import type { FastifyInstance } from 'fastify'; import { parseSiteConfig } from '../src/config.ts'; import { createSessionToken } from '../src/auth.ts'; import { buildApp } from '../src/server.ts'; import { mediaUrls } from '../src/render/pages.ts'; import { FakePipeline, pngBytes, tempDir, testClock } from './support/harness.ts'; const ASSETS = path.join(import.meta.dirname, '..', 'public'); const SECRET = 'test-session-secret'; let dataDir: string; let app: FastifyInstance; const CLOCK = testClock('2026-05-01T06:00:00.000Z'); function config(overrides: Record = {}) { return parseSiteConfig({ site: 'menu.example.test', brand: { name: 'Hops n Grains', tagline: 'Handcrafted Microbrewery' }, sections: [ { slug: 'food', label: 'Food', order: 1 }, { slug: 'bar', label: 'Bar', order: 2 }, { slug: 'wine', label: 'Wine', order: 3 }, ], ...overrides, }); } async function build( options: { config?: ReturnType } = {}, ): Promise { const built = await buildApp({ config: options.config ?? config(), dataDir, sessionSecret: SECRET, adminPasswordHash: null, assetsDir: ASSETS, secureCookies: false, logLevel: 'silent', pipeline: new FakePipeline(), now: CLOCK.now, }); await built.ready(); return built; } beforeEach(async () => { dataDir = await tempDir('routes'); app = await build(); }); afterEach(async () => { await app.close(); await rm(dataDir, { recursive: true, force: true }); }); async function seedPublishedBar() { const store = app.menu.store; const set = await store.create('bar', { label: 'Bar Menu', validUntil: '2026-06-26' }); const first = await store.addPage(set.id, { data: pngBytes('one'), originalName: 'a.jpg' }); const second = await store.addPage(set.id, { data: pngBytes('two'), originalName: 'b.jpg' }); const published = await store.publish(set.id); return { set, first, second, published }; } describe('an empty Site', () => { it('shows the empty state rather than a bare page', async () => { const response = await app.inject({ method: 'GET', url: '/' }); assert.equal(response.statusCode, 200); assert.match(response.body, /The menus are being updated/); assert.match(response.headers['content-type'] ?? '', /text\/html/); }); it('404s a Section with no Current set, offering the Sections that exist', async () => { const response = await app.inject({ method: 'GET', url: '/bar/' }); assert.equal(response.statusCode, 404); assert.match(response.body, /That menu does not exist/); assert.match(response.body, /href="\/bar\/"/); }); }); describe('the landing page', () => { it('lists Sections that have a Current Menu Set, with page counts', async () => { await seedPublishedBar(); const response = await app.inject({ method: 'GET', url: '/' }); assert.equal(response.statusCode, 200); assert.match(response.body, /Hops n Grains/); assert.match(response.body, /Handcrafted Microbrewery/); assert.match(response.body, /class="shelf" href="\/bar\/"/); assert.match(response.body, /Bar<\/span>/); assert.match(response.body, /2 pages/); assert.match( response.body, /class="shelf__peek"/, 'the shelf shows a sliver of the menu paper', ); assert.doesNotMatch(response.body, /shelf__name">Wine/); }); it('escapes a label that tries to inject markup', async () => { const store = app.menu.store; const set = await store.create('bar', { label: '' }); await store.addPage(set.id, { data: pngBytes('one'), originalName: 'a.jpg' }); await store.publish(set.id); const response = await app.inject({ method: 'GET', url: '/bar/' }); assert.equal(response.statusCode, 200); assert.doesNotMatch(response.body, /