import { test, after } from 'node:test' import assert from 'node:assert/strict' import express from 'express' import { mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' // P10 Task 3: Umschalt-Weiche – erst bei KUNDENANSICHT_NEU=on liefert sie die neuen öffentlichen Seiten, das Manifest und die Fassung-6-Adresse const aw = await startAppwriteMock({ schema: { customerViewLog: schemaKeys('customerViewLog') } }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { startTestApp } = await import('./support/testApp.js') const { umschaltSeitenRouter, UMSCHALT_SEITEN, fassung6Aus } = await import('../server/routes/umschaltSeiten.js') const { config } = await import('../server/config.js') const { termsDocFor } = await import('../server/services/products.js') const publicDir = fileURLToPath(new URL('../public/', import.meta.url)) let modus = 'off' let fassung6 = null const router = express.Router() router.use(umschaltSeitenRouter({ publicDir, modus: () => modus, fassung6: () => fassung6 })) router.use(express.static(publicDir)) const app = await startTestApp({ '/': router }) after(() => Promise.all([app.close(), aw.close(), st.close()])) const holen = async (pfad, basis = app.url, methode = 'GET') => { const r = await fetch(basis + pfad, { method: methode, redirect: 'manual' }) return { status: r.status, bytes: Buffer.from(await r.arrayBuffer()), headers: r.headers } } const datei = (rel) => readFileSync(path.join(publicDir, rel)) test('Übersicht N40: auch die öffentliche Auftragsseite (/auftrag.html#t= aus der Mail K31) ist eine Zeile der Weiche', () => { assert.equal(UMSCHALT_SEITEN['/auftrag.html'], 'neu/oeffentlich/auftrag.html') }) for (const [pfad, neu] of Object.entries(UMSCHALT_SEITEN)) { test(`${pfad}: off und pilot liefern die Datei von heute, on die neue (no-cache)`, async () => { for (const m of ['off', 'pilot']) { modus = m const r = await holen(pfad) assert.ok(r.status === 200 && r.bytes.equals(datei(pfad.slice(1))), `${m} ${pfad}`) } modus = 'on' const r = await holen(pfad) assert.ok(r.status === 200 && r.bytes.equals(datei(neu)), pfad) assert.equal(r.headers.get('cache-control'), 'no-cache') }) } test('Manifest bei on ist JSON', async () => { modus = 'on' assert.match((await holen('/manifest.webmanifest')).headers.get('content-type'), /json/) }) // P10 Task 14: entfällt (Parameter fassung6 verschwindet, Fassung 6 liegt dann unter /abo-bedingungen.html) test('Fassung 6: Seite und PDF kommen bei on aus der Neu-Fassung, sonst bleibt die Datei von heute', async () => { fassung6 = { url: '/neu/oeffentlich/offline.html', pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf' } // Stellvertreter-Dateien modus = 'pilot' assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('abo-bedingungen.html'))) modus = 'on' assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('neu/oeffentlich/offline.html'))) assert.ok((await holen('/WEBklar-Abo-Vertragsunterlagen.pdf')).bytes.equals(datei('WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf'))) fassung6 = null assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('abo-bedingungen.html'))) }) // P10 Task 14: entfällt (fassung6Aus verschwindet, public/neu/rechtliches/ wird zu Weiterleitungen) test('Standard-Fassung 6 (fassung6Aus): bei on kommen Seite und PDF der Fassung 6 aus public/neu/rechtliches/', async () => { const f6 = fassung6Aus() assert.deepEqual([f6.url, f6.pdfFile], ['/neu/rechtliches/abo-bedingungen.html', 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf']) const echt = express.Router() echt.use(umschaltSeitenRouter({ publicDir, modus: () => modus })) echt.use(express.static(publicDir)) const a3 = await startTestApp({ '/': echt }) try { modus = 'pilot' assert.ok((await holen('/abo-bedingungen.html', a3.url)).bytes.equals(datei('abo-bedingungen.html'))) modus = 'on' assert.ok((await holen('/abo-bedingungen.html', a3.url)).bytes.equals(datei('neu/rechtliches/abo-bedingungen.html'))) assert.ok((await holen('/WEBklar-Abo-Vertragsunterlagen.pdf', a3.url)).bytes.equals(datei('neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf'))) } finally { await a3.close() } }) // P10 Task 14: fällt mit dem Parameter fassung6 weg oder prüft dann die Ziele der Weiterleitungen test('Pfadwächter: eine Adresse außerhalb von public/ wird nie geliefert', async () => { modus = 'on' fassung6 = { url: '/../../package.json', pdfFile: '../../server/config.js' } for (const p of ['/abo-bedingungen.html', '/WEBklar-Abo-Vertragsunterlagen.pdf']) assert.ok((await holen(p)).bytes.equals(datei(p.slice(1))), p) }) test('fehlende neue Datei: Rückfall auf die alte statt 500', async () => { const tmp = mkdtempSync(path.join(tmpdir(), 'kn-weiche-')) writeFileSync(path.join(tmp, 'login.html'), 'ALT') const a2 = express() a2.use(umschaltSeitenRouter({ publicDir: tmp, modus: () => 'on', fassung6: () => null })) a2.use(express.static(tmp)) const srv = await new Promise((res) => { const s = a2.listen(0, '127.0.0.1', () => res(s)) }) try { const r = await holen('/login.html', `http://127.0.0.1:${srv.address().port}`) assert.deepEqual([r.status, r.bytes.toString()], [200, 'ALT']) // der Rückfall erbt kein no-cache der Weiche (die Datei von heute behält ihren eigenen Cache-Kopf) assert.notEqual(r.headers.get('cache-control'), 'no-cache') } finally { srv.close() unlinkSync(path.join(tmp, 'login.html')) rmdirSync(tmp) } }) test('nur GET und HEAD: HEAD liefert die neue Seite ohne Inhalt, POST bleibt unberührt', async () => { modus = 'on' fassung6 = null const kopf = await holen('/login.html', app.url, 'HEAD') assert.deepEqual([kopf.status, kopf.bytes.length, kopf.headers.get('cache-control')], [200, 0, 'no-cache']) assert.equal(Number(kopf.headers.get('content-length')), datei('neu/oeffentlich/login.html').length) const post = await holen('/login.html', app.url, 'POST') assert.equal(post.status, 404) }) test('Groß- und Kleinschreibung: /Login.html wird nicht umgeleitet (wie express.static)', async () => { modus = 'on' assert.equal((await holen('/Login.html')).status, 404) }) test('echter Schalter: der Router mit den Vorgaben (wie in server/index.js) folgt config.kundenansichtNeu, nur on ändert Seiten', async () => { const standard = express.Router() standard.use(umschaltSeitenRouter({ publicDir })) // ohne modus und fassung6: genau der Aufruf in server/index.js standard.use(express.static(publicDir)) const echt = await startTestApp({ '/': standard }) const gemerkt = { ...config.kundenansichtNeu } const aboNeu = datei(termsDocFor(config.abo.termsVersionNeu).url.slice(1)) const faelle = [ // [mode, pilotEmails, liefert die Weiche die neuen Seiten?] ['off', [], false], ['off', ['*'], false], ['pilot', [], false], ['pilot', ['*'], false], ['pilot', ['kunde@example.invalid'], false], ['an', ['*'], false], ['', ['*'], false], [undefined, ['*'], false], ['on', [], true], ['on', ['*'], true], ] try { for (const [mode, pilotEmails, neu] of faelle) { Object.assign(config.kundenansichtNeu, { mode, pilotEmails }) const fall = `${mode} [${pilotEmails}]` for (const [pfad, rel] of Object.entries(UMSCHALT_SEITEN)) { const r = await holen(pfad, echt.url) assert.ok(r.status === 200 && r.bytes.equals(datei(neu ? rel : pfad.slice(1))), `${fall} ${pfad}`) } const abo = await holen('/abo-bedingungen.html', echt.url) assert.ok(abo.status === 200 && abo.bytes.equals(neu ? aboNeu : datei('abo-bedingungen.html')), `${fall} /abo-bedingungen.html`) } } finally { Object.assign(config.kundenansichtNeu, gemerkt) await echt.close() } }) test('server/index.js hängt die Weiche hinter P0s Weichen und vor express.static ein', () => { const index = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8') const reihe = ["app.get('/dashboard.html', dashboardWeiche(", 'app.use(umschaltSeitenRouter(', 'app.use(express.static(publicDir))'].map((s) => index.indexOf(s)) assert.ok(reihe[0] > -1 && reihe.every((n, i) => i === 0 || n > reihe[i - 1]), reihe.join(',')) })