import { test, after, beforeEach, afterEach } from 'node:test' import assert from 'node:assert/strict' import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import http from 'node:http' import path from 'node:path' import express from 'express' import sharp from 'sharp' import { startAppwriteMock } from './support/appwriteMock.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' import { SHOP_DATETIMES } from '../server/services/shop/schema.js' // Oeffentliche Schnittstelle des Webshops, lesender Teil (F1): Konfiguration samt Artikeln und Artikelbilder. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), shopConfig: schemaKeys('shopConfig'), shopProducts: schemaKeys('shopProducts'), shopOrders: schemaKeys('shopOrders'), }, datetimes: SHOP_DATETIMES, defaultLimit: 25, }) useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) const { config } = await import('../server/config.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { startTestApp, send } = await import('./support/testApp.js') const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js') const K = await import('../server/services/shop/kontext.js') const { normalisiereProdukt } = await import('../server/services/shop/produkte.js') const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js') const { rechtstexteHash, GEWAEHRLEISTUNG_BILD } = await import('../server/services/shop/rechtstexte.js') const { formToken, pruefeFormToken } = await import('../server/services/shop/token.js') const { darfAnfrage, resetTermineLimitsForTests, zaehle } = await import('../server/services/termine/limits.js') const { default: router, setzeUhrFuerTests, resetPublicShopForTests } = await import('../server/routes/publicShop.js') const app = await startTestApp({ '/': kundenansichtGuard, '/api/public/shop/v1': router }) const uploadDir = mkdtempSync(path.join(tmpdir(), 'shop-public-')) after(() => Promise.all([app.close(), aw.close()]).then(() => rmSync(uploadDir, { recursive: true, force: true }))) const BASIS = '/api/public/shop/v1' const JETZT = new Date('2026-10-05T12:00:00.000Z') const KUNDE = 'c_kunde01' const PROJEKT = 'p_projekt01' const FEATURE = 'f_p_projekt01_shop' const SITE_KEY = 'abcdefghijklmnop' const BILD_ID = 'b1b1b1b1-aaaa-bbbb-cccc-111122223333' const FREMD_BILD = 'f0f0f0f0-aaaa-bbbb-cccc-111122223333' const VORHER = { mode: config.shop.mode, pilot: [...config.shop.pilotEmails], uploadDir: config.shop.uploadDir, basis: config.publicBaseUrl, formMin: config.shop.formMinSekunden, } const EINSTELLUNGEN = { ...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)), kleinunternehmer: true, lieferzeit: '2–4 Werktage', shopname: 'Brot & Mehr', akzent: '#aa3300', anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '0631 123', ustId: 'DE123456789' }, versand: { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 }, abholung: { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' }, zahlungsarten: ['card', 'sepa_debit'], mindestbestellCent: 1000, kleinunternehmer: false, } const TEXTE = { agb: 'AGB-Text', widerrufsbelehrung: 'Widerruf-Text', widerrufsformular: 'Formular-Text', impressumUrl: 'https://brot.example/impressum', datenschutzUrl: 'https://brot.example/datenschutz', } const featureDoc = (over = {}) => ({ $id: FEATURE, customerId: KUNDE, projectId: PROJEKT, featureKey: 'shop', siteKey: SITE_KEY, state: 'active', enabled: true, ...over }) const shopDoc = (over = {}) => ({ $id: FEATURE, customerId: KUNDE, projectId: PROJEKT, siteKey: SITE_KEY, anfrageStatus: 'angenommen', stripeAccountId: 'acct_geheim123', stripeStatus: JSON.stringify({ charges_enabled: true, payouts_enabled: true }), einstellungen: JSON.stringify(EINSTELLUNGEN), rechtstexte: JSON.stringify({ entwurf: { ...TEXTE, agb: 'ENTWURF-AGB' }, bestaetigt: { texte: TEXTE, hash: rechtstexteHash(TEXTE, EINSTELLUNGEN), at: '2026-10-05T10:00:00.000Z', von: 'kunde@example.invalid' }, }), geoeffnet: true, version: 3, ...over, }) let zaehler = 0 const artikel = (id, over = {}, produktOver = {}) => { const r = normalisiereProdukt({ name: `Artikel ${id}`, preisCent: 450, ...produktOver }, { einstellungen: EINSTELLUNGEN }) assert.equal(r.fehler, undefined, JSON.stringify(r.fehler)) return { $id: id, shopId: FEATURE, customerId: KUNDE, aktiv: r.produkt.aktiv, sort: ++zaehler, name: r.produkt.name, produkt: JSON.stringify({ ...r.produkt, ...(produktOver.bilder ? { bilder: produktOver.bilder } : {}) }), ...over } } async function aufbau({ feature = {}, shop = {}, produkte, kunde = { email: 'inhaber@brot.example' } } = {}) { for (const col of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'websiteProjects', 'customers']) { for (const id of [...aw.docs(col).keys()]) await deleteDocument(col, id) } K.leereCache() resetTermineLimitsForTests() aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Brotladen' }) if (kunde) aw.seed('customers', { $id: KUNDE, ...kunde }) aw.seed('portalFeatures', featureDoc(feature)) if (shop !== null) aw.seed('shopConfig', shopDoc(shop)) const liste = produkte ?? [ artikel('a1', {}, { kategorie: 'Brot', bilder: [BILD_ID] }), artikel('a2', {}, { name: 'Zweites Brot', kategorie: 'Brot', bestand: 3 }), artikel('a3', {}, { name: 'Kuchen', kategorie: 'Süßes' }), artikel('a4', { aktiv: false }, { name: 'Verstecktes', kategorie: 'Geheim' }), artikel('a5', {}, { name: 'Ohne Kategorie' }), ] for (const p of liste) aw.seed('shopProducts', p) } const kopfzeile = (res, name) => res.headers.get(name) let ip let n = 0 const api = (pfad, { ip: adresse = ip, headers = {} } = {}) => send('GET', BASIS + pfad, { headers: { 'x-forwarded-for': adresse, ...headers } }) // Nichts Internes in irgendeiner Antwort const VERBOTEN = ['customerId', 'projectId', 'featureId', 'acct_', 'stripeAccountId', 'stripeStatus', '$id', '$collectionId', '$createdAt', 'inhaber@brot.example', KUNDE, PROJEKT, FEATURE, 'DE123456789', 'ENTWURF-AGB'] const ohneInternes = (body) => { const s = JSON.stringify(body) for (const v of VERBOTEN) assert.ok(!s.includes(v), `interner Wert in der Antwort: ${v}`) } beforeEach(async () => { zaehler = 0 ip = `10.1.${Math.floor(++n / 250)}.${(n % 250) + 1}` config.shop.mode = 'on' config.shop.pilotEmails = [] config.shop.uploadDir = uploadDir config.publicBaseUrl = 'https://portal.example' setzeUhrFuerTests(() => JETZT) await aufbau() }) afterEach(() => { resetPublicShopForTests() config.shop.mode = VORHER.mode config.shop.pilotEmails = VORHER.pilot config.shop.uploadDir = VORHER.uploadDir config.publicBaseUrl = VORHER.basis config.shop.formMinSekunden = VORHER.formMin }) test('GET /:siteKey: Shop offen, Aufbau der Antwort und Kopfzeilen', async () => { const res = await api(`/${SITE_KEY}`) assert.equal(res.status, 200) assert.equal(kopfzeile(res, 'access-control-allow-origin'), '*') assert.equal(kopfzeile(res, 'cache-control'), 'no-store') assert.equal(kopfzeile(res, 'x-content-type-options'), 'nosniff') const b = res.body assert.equal(b.aktiv, true) assert.equal(b.offen, true) assert.equal(b.nurWiderruf, undefined) assert.equal(b.shop.name, 'Brot & Mehr') assert.equal(b.shop.akzent, '#aa3300') assert.deepEqual(b.shop.anbieter, { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '0631 123' }) assert.deepEqual(b.shop.lieferlaender, [{ code: 'DE', name: 'Deutschland' }, { code: 'AT', name: 'Österreich' }]) assert.deepEqual(b.shop.abholung, { aktiv: true, adresse: 'Hauptstraße 5, 67655 Kaiserslautern', hinweis: 'Mo–Fr 8–12 Uhr' }) assert.equal(b.shop.lieferzeit, EINSTELLUNGEN.lieferzeit) assert.equal(b.shop.zahlungsarten, 'Kreditkarte, Apple Pay, Google Pay, SEPA-Lastschrift') assert.deepEqual(b.shop.zahlungsartenListe, ['Kreditkarte, Apple Pay, Google Pay', 'SEPA-Lastschrift']) assert.equal(b.shop.versandText, 'Versand nach Deutschland 4,90 €, Österreich 9,90 €, ab 50,00 € versandkostenfrei') assert.equal(b.shop.mindestbestellText, 'Mindestbestellwert 10,00 €') assert.equal(b.shop.kleinunternehmer, false) assert.equal(b.shop.steuerHinweisKurz, 'Alle Preise inkl. MwSt.') assert.deepEqual(b.kategorien, ['Brot', 'Süßes']) assert.deepEqual(b.produkte.map((p) => p.id), ['a1', 'a2', 'a3', 'a5']) assert.equal(b.produkte[0].preisText, '4,50 €') assert.deepEqual(b.produkte[0].bilder, [{ klein: `${BASIS}/${SITE_KEY}/bild/${BILD_ID}-s.webp`, gross: `${BASIS}/${SITE_KEY}/bild/${BILD_ID}-l.webp` }]) assert.equal(b.rechtstexte.agb, 'AGB-Text') assert.equal(b.rechtstexte.widerrufsbelehrung, 'Widerruf-Text') assert.equal(b.rechtstexte.widerrufsformular, 'Formular-Text') assert.match(b.rechtstexte.versandUndZahlung, /Liefergebiet/) assert.equal(b.rechtstexte.impressumUrl, 'https://brot.example/impressum') assert.equal(b.rechtstexte.datenschutzUrl, 'https://brot.example/datenschutz') assert.equal(b.gewaehrleistung.bild, `https://portal.example${GEWAEHRLEISTUNG_BILD}`) assert.match(b.gewaehrleistung.text, /GESETZLICHE GEWÄHRLEISTUNG/) assert.equal(b.widerrufUrl, `https://portal.example/shop/${SITE_KEY}/widerruf`) ohneInternes(b) }) test('Kleinunternehmer-Hinweis, nur Abholung, ohne Mindestbestellwert', async () => { const e = { ...EINSTELLUNGEN, kleinunternehmer: true, mindestbestellCent: null, versand: { ...EINSTELLUNGEN.versand, aktiv: false } } await aufbau({ shop: { einstellungen: JSON.stringify(e) } }) const b = (await api(`/${SITE_KEY}`)).body assert.equal(b.shop.steuerHinweisKurz, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.') assert.equal(b.shop.kleinunternehmer, true) assert.deepEqual(b.shop.lieferlaender, []) assert.equal(b.shop.versandText, 'Nur Abholung') assert.equal(b.shop.mindestbestellText, '') }) test('Formular-Token der Antwort gehoert zum siteKey und ist nach der Wartezeit gueltig', async () => { const b = (await api(`/${SITE_KEY}`)).body assert.match(b.formToken, /^[0-9a-z]+\.[A-Za-z0-9_-]{43}$/) const spaeter = new Date(JETZT.getTime() + 10_000) assert.equal(pruefeFormToken(SITE_KEY, b.formToken, spaeter), 'ok') assert.equal(pruefeFormToken(SITE_KEY, b.formToken, new Date(JETZT.getTime() + 1000)), 'zu_schnell') assert.equal(pruefeFormToken('zzzzzzzzzzzzzzzz', b.formToken, spaeter), 'ungueltig') assert.equal(pruefeFormToken(SITE_KEY, `${b.formToken}x`, spaeter), 'ungueltig') assert.equal(pruefeFormToken(SITE_KEY, b.formToken, new Date(JETZT.getTime() + 3 * 3600_000)), 'abgelaufen') config.shop.formMinSekunden = 0 assert.equal(pruefeFormToken(SITE_KEY, formToken(SITE_KEY, JETZT), JETZT), 'ok') }) test('Reservierungen offener Bestellungen mindern den Bestand in der Antwort', async () => { const bestand = (await api(`/${SITE_KEY}`)).body.produkte.find((p) => p.id === 'a2') assert.equal(bestand.verfuegbar, true) aw.seed('shopOrders', { $id: 'o_1', shopId: FEATURE, status: 'offen', ablaufAt: new Date(JETZT.getTime() + 10 * 60_000).toISOString(), bestellung: JSON.stringify({ positionen: [{ produktId: 'a2', menge: 3 }] }), }) const ausverkauft = (await api(`/${SITE_KEY}`)).body.produkte.find((p) => p.id === 'a2') assert.equal(ausverkauft.verfuegbar, false) }) test('noch keine bestaetigten Rechtstexte: rechtstexte null, Shop geschlossen', async () => { await aufbau({ shop: { rechtstexte: JSON.stringify({ entwurf: TEXTE, bestaetigt: null }) } }) const b = (await api(`/${SITE_KEY}`)).body assert.equal(b.aktiv, true) assert.equal(b.offen, false) assert.equal(b.rechtstexte, null) assert.ok(b.produkte.length > 0) }) test('nicht geoeffnet (Haendler hat nicht geoeffnet): offen false', async () => { await aufbau({ shop: { geoeffnet: false } }) const b = (await api(`/${SITE_KEY}`)).body assert.equal(b.aktiv, true) assert.equal(b.offen, false) }) test('abgeschalteter Shop (disabled): nur Widerruf, keine Artikel', async () => { await aufbau({ feature: { state: 'disabled' } }) const res = await api(`/${SITE_KEY}`) assert.equal(res.status, 200) assert.deepEqual(Object.keys(res.body).sort(), ['aktiv', 'formToken', 'nurWiderruf', 'offen', 'shop', 'widerrufUrl']) assert.equal(res.body.aktiv, true) assert.equal(res.body.offen, false) assert.equal(res.body.nurWiderruf, true) assert.deepEqual(Object.keys(res.body.shop).sort(), ['akzent', 'anbieter', 'name']) assert.equal(res.body.shop.name, 'Brot & Mehr') assert.equal(res.body.widerrufUrl, `https://portal.example/shop/${SITE_KEY}/widerruf`) ohneInternes(res.body) }) test('aktiv:false ohne Orakel: unbekannt, ungueltig, falscher Zustand, Modus off', async () => { const nein = async (pfad) => { const res = await api(pfad) assert.equal(res.status, 200, pfad) assert.deepEqual(res.body, { aktiv: false }, pfad) assert.equal(kopfzeile(res, 'access-control-allow-origin'), '*') } await nein('/zzzzzzzzzzzzzzzz') // unbekannt await nein('/abc') // kein siteKey await nein('/ABCDEFGHIJKLMNOP') // falsche Schreibweise await aufbau({ feature: { state: 'ordered' } }) await nein(`/${SITE_KEY}`) await aufbau({ feature: { state: 'off' } }) await nein(`/${SITE_KEY}`) await aufbau({ shop: null }) // kein shopConfig await nein(`/${SITE_KEY}`) await aufbau() config.shop.mode = 'on' assert.equal((await api(`/${SITE_KEY}`)).body.aktiv, true) }) test('M-2: Modus off und Pilotkreis ohne Besitzer sperren das Bestellen, nicht die Widerrufsseite: GET liefert nurWiderruf, Bestellrouten bleiben aktiv:false', async () => { const nurWiderruf = (body) => { assert.equal(body.aktiv, true) assert.equal(body.offen, false) assert.equal(body.nurWiderruf, true) assert.ok(body.formToken && body.widerrufUrl.endsWith(`/shop/${SITE_KEY}/widerruf`)) assert.equal(body.shop.name, 'Brot & Mehr') assert.equal(body.produkte, undefined) assert.equal(body.rechtstexte, undefined) } config.shop.mode = 'off' nurWiderruf((await api(`/${SITE_KEY}`)).body) assert.equal((await api(`/${SITE_KEY}/bild/${BILD_ID}-s.webp`)).status, 404) K.leereCache() config.shop.mode = 'pilot' config.shop.pilotEmails = ['andere@example.com'] nurWiderruf((await api(`/${SITE_KEY}`)).body) K.leereCache() config.shop.pilotEmails = ['inhaber@brot.example'] assert.equal((await api(`/${SITE_KEY}`)).body.nurWiderruf, undefined, 'im Pilotkreis wie gewohnt') // unbekannter Shop und andere Zustaende bleiben aktiv:false, auch bei off config.shop.mode = 'off' await aufbau({ feature: { state: 'off' } }) assert.deepEqual((await api(`/${SITE_KEY}`)).body, { aktiv: false }) assert.deepEqual((await api('/zzzzzzzzzzzzzzzz')).body, { aktiv: false }) }) test('Pilotmodus: nur Besitzer auf der Pilotliste bekommen den Shop, alle anderen nur die Widerrufsseite', async () => { config.shop.mode = 'pilot' config.shop.pilotEmails = ['andere@example.com'] const gesperrt = (await api(`/${SITE_KEY}`)).body assert.deepEqual([gesperrt.aktiv, gesperrt.nurWiderruf, gesperrt.produkte], [true, true, undefined]) K.leereCache() config.shop.pilotEmails = ['inhaber@brot.example'] assert.equal((await api(`/${SITE_KEY}`)).body.aktiv, true) assert.equal((await api(`/${SITE_KEY}`)).body.nurWiderruf, undefined) K.leereCache() config.shop.pilotEmails = ['*'] assert.equal((await api(`/${SITE_KEY}`)).body.nurWiderruf, undefined) // Kunde ohne Dokument: nie freigeschaltet await aufbau({ kunde: null }) config.shop.pilotEmails = ['inhaber@brot.example'] assert.equal((await api(`/${SITE_KEY}`)).body.nurWiderruf, true) // im Pilotmodus gilt dasselbe fuer die Bilder await aufbau() config.shop.pilotEmails = ['andere@example.com'] assert.equal((await api(`/${SITE_KEY}/bild/${BILD_ID}-s.webp`)).status, 404) }) // ---------------------------------------------------------------- Bilder async function legeBildAb(id = BILD_ID, siteKey = SITE_KEY) { const ordner = path.join(uploadDir, siteKey) mkdirSync(ordner, { recursive: true }) const webp = await sharp({ create: { width: 4, height: 4, channels: 3, background: '#cc3300' } }).webp().toBuffer() writeFileSync(path.join(ordner, `${id}-s.webp`), webp) writeFileSync(path.join(ordner, `${id}-l.webp`), webp) writeFileSync(path.join(ordner, `${id}.json`), JSON.stringify({ customerId: KUNDE, shopId: FEATURE, createdAt: JETZT.toISOString(), claimed: true, w: 4, h: 4 })) return webp } test('Bild: Auslieferung mit Cache, nosniff und CORS', async () => { const webp = await legeBildAb() for (const g of ['s', 'l']) { const res = await fetch(`${app.url}${BASIS}/${SITE_KEY}/bild/${BILD_ID}-${g}.webp`, { headers: { 'x-forwarded-for': ip } }) assert.equal(res.status, 200) assert.equal(res.headers.get('content-type'), 'image/webp') assert.equal(res.headers.get('cache-control'), 'public, max-age=86400') assert.equal(res.headers.get('x-content-type-options'), 'nosniff') assert.equal(res.headers.get('access-control-allow-origin'), '*') assert.deepEqual(Buffer.from(await res.arrayBuffer()), webp) } }) test('Bild: 404 bei fremdem, unbekanntem, inaktivem Artikel, schlechtem Namen und fehlender Datei', async () => { await legeBildAb() await legeBildAb(FREMD_BILD) // liegt im Ordner, gehoert aber zu keinem Artikel const vier = async (pfad) => { const res = await api(pfad) assert.equal(res.status, 404, pfad) assert.deepEqual(res.body, { fehler: { _: 'Nicht gefunden' } }, pfad) assert.equal(kopfzeile(res, 'cache-control'), 'no-store') return res } await vier(`/${SITE_KEY}/bild/${FREMD_BILD}-s.webp`) await vier(`/${SITE_KEY}/bild/${BILD_ID}-m.webp`) // Groesse await vier(`/${SITE_KEY}/bild/${BILD_ID}-s.png`) await vier(`/${SITE_KEY}/bild/..%2F${BILD_ID}-s.webp`) await vier(`/${SITE_KEY}/bild/${BILD_ID.toUpperCase()}-s.webp`) await vier(`/zzzzzzzzzzzzzzzz/bild/${BILD_ID}-s.webp`) // fremder siteKey // Artikel inaktiv await aufbau({ produkte: [artikel('a1', { aktiv: false }, { bilder: [BILD_ID] })] }) await legeBildAb() await vier(`/${SITE_KEY}/bild/${BILD_ID}-s.webp`) // Artikel mit Bild, aber Datei fehlt (kein Sidecar) await aufbau() rmSync(path.join(uploadDir, SITE_KEY), { recursive: true, force: true }) await vier(`/${SITE_KEY}/bild/${BILD_ID}-s.webp`) // abgeschalteter Shop liefert keine Bilder await legeBildAb() await aufbau({ feature: { state: 'disabled' } }) await legeBildAb() await vier(`/${SITE_KEY}/bild/${BILD_ID}-s.webp`) }) // ---------------------------------------------------------------- Grenzen test('Grenze je IP: 120 je Minute, danach 429 mit Retry-After', async () => { for (let i = 0; i < 120; i++) assert.equal(darfAnfrage(ip, JETZT), true) const res = await api(`/${SITE_KEY}`) assert.equal(res.status, 429) assert.equal(kopfzeile(res, 'retry-after'), '60') assert.equal(kopfzeile(res, 'access-control-allow-origin'), '*') assert.deepEqual(Object.keys(res.body), ['fehler']) // andere IP ist nicht betroffen assert.equal((await api(`/${SITE_KEY}`, { ip: '203.0.113.9' })).status, 200) }) test('Grenze je Shop: 600 je Minute ueber alle IPs, unbekannte Shops zaehlen nicht', async () => { for (let i = 0; i < 600; i++) assert.equal(zaehle(`shop:${SITE_KEY}`, 600, 60_000, JETZT), true) const res = await api(`/${SITE_KEY}`) assert.equal(res.status, 429) assert.equal(kopfzeile(res, 'retry-after'), '60') // unbekannter Shop: weiter aktiv:false statt 429 (kein Zaehler je unbekanntem Schluessel) assert.deepEqual((await api('/zzzzzzzzzzzzzzzz')).body, { aktiv: false }) }) test('Fenster laeuft ab: nach einer Minute ist die Grenze weg', async () => { for (let i = 0; i < 600; i++) zaehle(`shop:${SITE_KEY}`, 600, 60_000, JETZT) assert.equal((await api(`/${SITE_KEY}`)).status, 429) setzeUhrFuerTests(() => new Date(JETZT.getTime() + 61_000)) assert.equal((await api(`/${SITE_KEY}`)).status, 200) }) // ---------------------------------------------------------------- Rest test('unbekannter Pfad: 404 ohne Rohtext', async () => { const r1 = await api(`/${SITE_KEY}/gibtsnicht`) assert.equal(r1.status, 404) assert.deepEqual(r1.body, { fehler: { _: 'Nicht gefunden' } }) assert.equal(kopfzeile(r1, 'x-content-type-options'), 'nosniff') }) test('eigener Parser: kaputter Rumpf 400, zu grosser Rumpf 413 (Router allein, ohne globalen Parser)', async () => { const server = http.createServer(express().use('/api/public/shop/v1', router)) await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) const url = `http://127.0.0.1:${server.address().port}${BASIS}/${SITE_KEY}` try { const kaputt = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{kaputt' }) assert.equal(kaputt.status, 400) assert.deepEqual(await kaputt.json(), { fehler: { _: 'Ungültige Anfrage.' } }) assert.equal(kaputt.headers.get('x-content-type-options'), 'nosniff') const gross = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ x: 'a'.repeat(20_000) }) }) assert.equal(gross.status, 413) assert.deepEqual(await gross.json(), { fehler: { _: 'Die Anfrage ist zu groß.' } }) } finally { server.closeAllConnections() await new Promise((resolve) => server.close(resolve)) } }) test('Fehler der Datenhaltung: 503 ohne Rohtext', async () => { // Datenhaltung nicht erreichbar: der Fehler (mit Adresse) gelangt nie nach aussen const alt = config.appwrite.endpoint config.appwrite.endpoint = 'http://127.0.0.1:1/v1' try { const res = await api(`/${SITE_KEY}`) assert.equal(res.status, 503) assert.deepEqual(res.body, { fehler: { _: 'Gerade nicht erreichbar' } }) assert.equal(kopfzeile(res, 'access-control-allow-origin'), '*') } finally { config.appwrite.endpoint = alt } })