Files
Webklar-Kundenbereich/test/publicShop.test.js
T
ClaudeandClaude Opus 5.5 1f56b86791 Shop Review B I-4: Steuerart und Lieferzeit ohne Vorbelegung
kleinunternehmer ist ab jetzt null (nicht gewaehlt), lieferzeit leer. istVollstaendig verlangt eine
bewusste Wahl (true oder false), der Shop kann sonst nicht geoeffnet werden. Im Reiter
Einstellungen gibt es zwei Pflicht-Radiobuttons ohne Vorauswahl; Preise, Artikel und Muster
rechnen bei null wie bei Regelbesteuerung. Tests und Fixtures angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:50:54 +00:00

430 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 'off'
await nein(`/${SITE_KEY}`)
config.shop.mode = 'on'
assert.equal((await api(`/${SITE_KEY}`)).body.aktiv, true)
})
test('Pilotmodus: nur Besitzer auf der Pilotliste', async () => {
config.shop.mode = 'pilot'
config.shop.pilotEmails = ['andere@example.com']
assert.deepEqual((await api(`/${SITE_KEY}`)).body, { aktiv: false })
K.leereCache()
config.shop.pilotEmails = ['inhaber@brot.example']
assert.equal((await api(`/${SITE_KEY}`)).body.aktiv, true)
K.leereCache()
config.shop.pilotEmails = ['*']
assert.equal((await api(`/${SITE_KEY}`)).body.aktiv, true)
// Kunde ohne Dokument: nie freigeschaltet
await aufbau({ kunde: null })
config.shop.pilotEmails = ['inhaber@brot.example']
assert.deepEqual((await api(`/${SITE_KEY}`)).body, { aktiv: false })
// 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
}
})