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>
394 lines
18 KiB
JavaScript
394 lines
18 KiB
JavaScript
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { mailSink } from './support/mailSink.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
|
||
|
||
// Oeffentliche Schnittstelle des Webshops, POST /:siteKey/widerruf (F3) samt Preflight: Herkunft, Honigtopf, Formular-Token,
|
||
// Fehlervertrag, Grenzen. Mails landen in der Mailsenke; nichts verlaesst den Prozess.
|
||
const aw = await startAppwriteMock({
|
||
schema: {
|
||
portalFeatures: schemaKeys('portalFeatures'),
|
||
shopConfig: schemaKeys('shopConfig'),
|
||
shopProducts: schemaKeys('shopProducts'),
|
||
shopOrders: schemaKeys('shopOrders'),
|
||
shopWiderrufe: schemaKeys('shopWiderrufe'),
|
||
termineOutbox: schemaKeys('termineOutbox'),
|
||
},
|
||
datetimes: { ...TERMINE_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 { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||
const K = await import('../server/services/shop/kontext.js')
|
||
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
|
||
const { formToken } = await import('../server/services/shop/token.js')
|
||
const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
|
||
const { default: router, setzeUhrFuerTests, resetPublicShopForTests } = await import('../server/routes/publicShop.js')
|
||
|
||
const app = await startTestApp({ '/api/public/shop/v1': router })
|
||
after(async () => {
|
||
setMailTransportForTests(null)
|
||
await Promise.all([app.close(), aw.close()])
|
||
})
|
||
|
||
const BASIS = '/api/public/shop/v1'
|
||
const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin (MESZ)
|
||
const KUNDE = 'c_kunde01'
|
||
const PROJEKT = 'p_projekt01'
|
||
const FEATURE = 'f_p_projekt01_shop'
|
||
const SITE_KEY = 'abcdefghijklmnop'
|
||
const O_LIVE = 'https://brot.example'
|
||
const O_PORTAL = 'https://portal.example'
|
||
const VORHER = { mode: config.shop.mode, pilot: [...config.shop.pilotEmails], basis: config.publicBaseUrl, formMin: config.shop.formMinSekunden, minAntwort: config.shop.minAntwortMs, from: config.smtp.from }
|
||
|
||
const EINSTELLUNGEN = {
|
||
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
|
||
kleinunternehmer: true,
|
||
lieferzeit: '2–4 Werktage',
|
||
shopname: 'Brot & Mehr',
|
||
anbieter: { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '', ustId: '' },
|
||
}
|
||
const EINGABE = { name: 'Max Mustermann', email: 'Max@Example.com', bestellnummer: '26-k7qm2x', nachricht: 'Ich widerrufe.' }
|
||
|
||
const bestellung = () => ({
|
||
$id: 'o_bestellung01',
|
||
shopId: FEATURE,
|
||
customerId: KUNDE,
|
||
bestellnummer: '26-K7QM2X',
|
||
status: 'bezahlt',
|
||
kaeuferEmail: 'max@example.com',
|
||
summeCent: 1200,
|
||
erstelltAt: JETZT.toISOString(),
|
||
bezahltAt: JETZT.toISOString(),
|
||
bestellung: JSON.stringify({ positionen: [{ name: 'Brot', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }], verlauf: [{ at: JETZT.toISOString(), was: 'angelegt' }] }),
|
||
})
|
||
|
||
let sink
|
||
let ip
|
||
let n = 0
|
||
const feature = (over = {}) => ({ $id: FEATURE, customerId: KUNDE, projectId: PROJEKT, featureKey: 'shop', siteKey: SITE_KEY, state: 'active', enabled: true, ...over })
|
||
const shop = () => ({
|
||
$id: FEATURE,
|
||
customerId: KUNDE,
|
||
projectId: PROJEKT,
|
||
siteKey: SITE_KEY,
|
||
anfrageStatus: 'angenommen',
|
||
einstellungen: JSON.stringify(EINSTELLUNGEN),
|
||
rechtstexte: JSON.stringify({ entwurf: {}, bestaetigt: null }),
|
||
geoeffnet: true,
|
||
version: 1,
|
||
})
|
||
|
||
async function aufbau({ state = 'active', mitBestellung = true } = {}) {
|
||
for (const col of ['portalFeatures', 'shopConfig', 'shopProducts', 'shopOrders', 'shopWiderrufe', 'termineOutbox', 'websiteProjects', 'customers']) {
|
||
for (const id of [...aw.docs(col).keys()]) await deleteDocument(config.collections[col], id)
|
||
}
|
||
K.leereCache()
|
||
resetOutboxForTests()
|
||
resetTermineLimitsForTests()
|
||
aw.seed('websiteProjects', { $id: PROJEKT, customerId: KUNDE, projectName: 'Brotladen', liveDomain: 'brot.example' })
|
||
aw.seed('customers', { $id: KUNDE, name: 'Erika Muster', email: 'inhaber@brot.example' })
|
||
aw.seed('portalFeatures', feature({ state }))
|
||
aw.seed('shopConfig', shop())
|
||
if (mitBestellung) aw.seed('shopOrders', bestellung())
|
||
}
|
||
|
||
const token = (alterMs = 10000) => formToken(SITE_KEY, new Date(JETZT.getTime() - alterMs))
|
||
const koerper = (over = {}) => ({ ...EINGABE, formToken: token(), ...over })
|
||
const post = (pfad, body, { origin = O_LIVE, ip: adresse = ip, headers = {} } = {}) =>
|
||
send('POST', BASIS + pfad, { body, headers: { 'x-forwarded-for': adresse, ...(origin ? { origin } : {}), ...headers } })
|
||
const widerruf = (body = koerper(), opts) => post(`/${SITE_KEY}/widerruf`, body, opts)
|
||
const kopf = (res, name) => res.headers.get(name)
|
||
const widerrufe = () => [...aw.docs('shopWiderrufe').values()]
|
||
const outbox = () => [...aw.docs('termineOutbox').values()]
|
||
|
||
beforeEach(async () => {
|
||
ip = `10.2.${Math.floor(++n / 250)}.${(n % 250) + 1}`
|
||
config.shop.mode = 'on'
|
||
config.shop.pilotEmails = []
|
||
config.shop.formMinSekunden = 3
|
||
config.shop.minAntwortMs = 0
|
||
config.publicBaseUrl = O_PORTAL
|
||
config.smtp.from = 'noreply@webklar.com'
|
||
setzeUhrFuerTests(() => JETZT)
|
||
sink = mailSink()
|
||
setMailTransportForTests(sink)
|
||
mock.method(console, 'warn', () => {})
|
||
await aufbau()
|
||
})
|
||
afterEach(async () => {
|
||
await warteAufVersand()
|
||
mock.restoreAll()
|
||
resetPublicShopForTests()
|
||
config.shop.mode = VORHER.mode
|
||
config.shop.pilotEmails = VORHER.pilot
|
||
config.shop.formMinSekunden = VORHER.formMin
|
||
config.shop.minAntwortMs = VORHER.minAntwort
|
||
config.publicBaseUrl = VORHER.basis
|
||
config.smtp.from = VORHER.from
|
||
})
|
||
|
||
const VERBOTEN = ['customerId', 'projectId', 'featureId', '$id', 'o_bestellung01', KUNDE, PROJEKT, FEATURE, 'inhaber@brot.example', 'max@example.com']
|
||
const ohneInternes = (body) => {
|
||
const s = JSON.stringify(body)
|
||
for (const v of VERBOTEN) assert.ok(!s.includes(v), `interner Wert in der Antwort: ${v}`)
|
||
}
|
||
|
||
test('Konfiguration: formMinSekunden und minAntwortMs sind Zahlen (Vorgaben 3 und 300)', () => {
|
||
assert.equal(typeof VORHER.formMin, 'number')
|
||
assert.equal(VORHER.formMin, 3)
|
||
assert.equal(VORHER.minAntwort, 300)
|
||
})
|
||
|
||
test('Erfolg mit passender Bestellung: { ok, eingang } ohne IDs, Widerruf gespeichert, G12 in der Warteschlange', async () => {
|
||
const res = await widerruf()
|
||
assert.equal(res.status, 200)
|
||
assert.deepEqual(res.body, { ok: true, eingang: '05.10.2026, 14:03 Uhr' })
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
|
||
assert.match(kopf(res, 'vary'), /Origin/)
|
||
assert.equal(kopf(res, 'cache-control'), 'no-store')
|
||
assert.equal(kopf(res, 'x-content-type-options'), 'nosniff')
|
||
ohneInternes(res.body)
|
||
await warteAufVersand()
|
||
|
||
const [doc] = widerrufe()
|
||
assert.equal(doc.orderId, 'o_bestellung01')
|
||
assert.equal(doc.shopId, FEATURE)
|
||
assert.equal(doc.email, 'max@example.com')
|
||
assert.equal(doc.bestellnummer, '26-K7QM2X')
|
||
const [g12] = outbox()
|
||
assert.equal(g12.art, 'G12')
|
||
assert.equal(g12.featureId, FEATURE)
|
||
const kaeufer = sink.sent.find((m) => m.to.some((t) => t.address === 'max@example.com'))
|
||
assert.ok(kaeufer, 'G12 versandt')
|
||
assert.ok(kaeufer.text.includes('05.10.2026, 14:03 Uhr'))
|
||
})
|
||
|
||
test('Erfolg ohne passende Bestellung: trotzdem erfasst und bestätigt', async () => {
|
||
await aufbau({ mitBestellung: false })
|
||
const res = await widerruf()
|
||
assert.equal(res.status, 200)
|
||
assert.equal(res.body.ok, true)
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 1)
|
||
assert.equal(widerrufe()[0].orderId, '')
|
||
assert.equal(outbox()[0].art, 'G12')
|
||
})
|
||
|
||
test('Herkunft: Live-Domain des Betriebs, Portal und gleiche Herkunft sind erlaubt', async () => {
|
||
assert.equal((await widerruf(koerper(), { origin: O_PORTAL })).status, 200)
|
||
const gleich = await widerruf(koerper(), { origin: null, headers: { origin: app.url } })
|
||
assert.equal(gleich.status, 200)
|
||
assert.equal(kopf(gleich, 'access-control-allow-origin'), app.url)
|
||
})
|
||
|
||
test('fremde Origin: 403 ohne Eintrag und ohne erlaubende CORS-Kopfzeile', async () => {
|
||
for (const origin of ['https://fremd.example', 'https://brot.example.evil.example', 'https://brot.example/', 'HTTPS://BROT.EXAMPLE', 'null', 'ftp://brot.example']) {
|
||
const res = await widerruf(koerper(), { origin })
|
||
assert.equal(res.status, 403, origin)
|
||
assert.deepEqual(res.body, { fehler: { _: 'Nicht erlaubt' } })
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), null)
|
||
}
|
||
assert.equal(widerrufe().length, 0)
|
||
})
|
||
|
||
test('fehlende Origin: 403', async () => {
|
||
const res = await widerruf(koerper(), { origin: null })
|
||
assert.equal(res.status, 403)
|
||
assert.deepEqual(res.body, { fehler: { _: 'Nicht erlaubt' } })
|
||
assert.equal(widerrufe().length, 0)
|
||
})
|
||
|
||
test('Preflight: erlaubte Origin bekommt 204 mit Methoden, Header, Max-Age und Vary: Origin (widerruf, pruefen, bestellung)', async () => {
|
||
for (const was of ['widerruf', 'pruefen', 'bestellung']) {
|
||
const res = await send('OPTIONS', `${BASIS}/${SITE_KEY}/${was}`, { headers: { 'x-forwarded-for': ip, origin: O_LIVE, 'access-control-request-method': 'POST' } })
|
||
assert.equal(res.status, 204, was)
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
|
||
assert.equal(kopf(res, 'access-control-allow-methods'), 'GET, POST, OPTIONS')
|
||
assert.equal(kopf(res, 'access-control-allow-headers'), 'Content-Type')
|
||
assert.equal(kopf(res, 'access-control-max-age'), '86400')
|
||
assert.match(kopf(res, 'vary'), /Origin/)
|
||
}
|
||
})
|
||
|
||
test('Preflight: fremde und fehlende Origin 403, unbekannter Shop und unbekannte Route ohne Verrat', async () => {
|
||
const fremd = await send('OPTIONS', `${BASIS}/${SITE_KEY}/widerruf`, { headers: { 'x-forwarded-for': ip, origin: 'https://fremd.example' } })
|
||
assert.equal(fremd.status, 403)
|
||
assert.equal(kopf(fremd, 'access-control-allow-origin'), null)
|
||
const ohne = await send('OPTIONS', `${BASIS}/${SITE_KEY}/widerruf`, { headers: { 'x-forwarded-for': ip } })
|
||
assert.equal(ohne.status, 403)
|
||
// unbekannter siteKey: nichts zu pruefen, nichts zu verraten
|
||
const unbekannt = await send('OPTIONS', `${BASIS}/zzzzzzzzzzzzzzzz/widerruf`, { headers: { 'x-forwarded-for': ip, origin: 'https://fremd.example' } })
|
||
assert.equal(unbekannt.status, 204)
|
||
assert.equal(kopf(unbekannt, 'access-control-allow-origin'), '*')
|
||
// andere Pfade sind keine POST-Routen
|
||
const anders = await send('OPTIONS', `${BASIS}/${SITE_KEY}/gibtsnicht`, { headers: { 'x-forwarded-for': ip, origin: O_LIVE } })
|
||
assert.equal(anders.status, 404)
|
||
})
|
||
|
||
test('Honigtopf kontakt_url gefüllt: gleiche Erfolgsform mit aktueller Zeit, nichts gespeichert, keine Mail', async () => {
|
||
for (const honig of ['http://spam.example', 'x', 0, false, ['a'], {}]) {
|
||
const res = await widerruf(koerper({ kontakt_url: honig }))
|
||
assert.equal(res.status, 200, JSON.stringify(honig))
|
||
assert.deepEqual(res.body, { ok: true, eingang: '05.10.2026, 14:03 Uhr' })
|
||
}
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 0)
|
||
assert.equal(outbox().length, 0)
|
||
assert.equal(sink.sent.length, 0)
|
||
// leer oder Leerraum zählt nicht als gefüllt
|
||
assert.equal((await widerruf(koerper({ kontakt_url: ' ' }))).status, 200)
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 1)
|
||
})
|
||
|
||
test('Honigtopf bei fremder Origin: trotzdem 403 (Herkunft vor Falle)', async () => {
|
||
assert.equal((await widerruf(koerper({ kontakt_url: 'x' }), { origin: 'https://fremd.example' })).status, 403)
|
||
})
|
||
|
||
test('Token zu schnell: Scheinerfolg, nichts gespeichert', async () => {
|
||
const res = await widerruf(koerper({ formToken: token(1000) }))
|
||
assert.equal(res.status, 200)
|
||
assert.deepEqual(res.body, { ok: true, eingang: '05.10.2026, 14:03 Uhr' })
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 0)
|
||
assert.equal(sink.sent.length, 0)
|
||
})
|
||
|
||
test('Token abgelaufen, fehlend, fremd oder kaputt: 400 "Bitte laden Sie die Seite neu." mit code neu_laden', async () => {
|
||
const faelle = [
|
||
token(3 * 60 * 60 * 1000),
|
||
undefined,
|
||
'',
|
||
'abc.def',
|
||
formToken('anderer-key-12345', new Date(JETZT.getTime() - 10000)),
|
||
12345,
|
||
'x'.repeat(500),
|
||
]
|
||
for (const t of faelle) {
|
||
const body = koerper({ formToken: t })
|
||
if (t === undefined) delete body.formToken
|
||
const res = await widerruf(body)
|
||
assert.equal(res.status, 400, String(t).slice(0, 20))
|
||
assert.deepEqual(res.body, { fehler: { _: 'Bitte laden Sie die Seite neu.' }, code: 'neu_laden' })
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
|
||
}
|
||
assert.equal(widerrufe().length, 0)
|
||
})
|
||
|
||
test('kein Objekt als Rumpf: 400; zu groß: 413', async () => {
|
||
for (const body of [[], 'text', 5]) {
|
||
const res = await send('POST', `${BASIS}/${SITE_KEY}/widerruf`, { body: JSON.stringify(body), headers: { 'x-forwarded-for': ip, origin: O_LIVE, 'content-type': 'application/json' } })
|
||
assert.ok(res.status === 400, JSON.stringify(body))
|
||
}
|
||
const gross = await widerruf(koerper({ nachricht: 'x'.repeat(20000) }))
|
||
assert.equal(gross.status, 413)
|
||
})
|
||
|
||
test('Ohne Bestellnummer: 400 fehler.nachricht, solange die Erklärung fehlt; mit Erklärung 200', async () => {
|
||
const leer = await widerruf(koerper({ bestellnummer: '', nachricht: '' }))
|
||
assert.equal(leer.status, 400)
|
||
assert.deepEqual(leer.body.fehler, { nachricht: 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.' })
|
||
const ok = await widerruf(koerper({ bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' }))
|
||
assert.equal(ok.status, 200)
|
||
assert.equal(ok.body.ok, true)
|
||
})
|
||
|
||
test('Prüfung: 400 mit fehler je Feld, nichts gespeichert', async () => {
|
||
const res = await widerruf(koerper({ name: 'M', email: 'keine-adresse', bestellnummer: '', nachricht: 'x'.repeat(2001) }))
|
||
assert.equal(res.status, 400)
|
||
assert.deepEqual(Object.keys(res.body.fehler).sort(), ['email', 'nachricht', 'name'])
|
||
for (const t of Object.values(res.body.fehler)) assert.match(t, /Bitte|darf/)
|
||
assert.equal(res.body.code, undefined)
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
|
||
assert.equal(widerrufe().length, 0)
|
||
})
|
||
|
||
test('Grenze je IP: nach 10 Versuchen je Stunde 429 mit Retry-After und Origin', async () => {
|
||
const eigene = '203.0.113.77'
|
||
for (let i = 0; i < 10; i++) assert.equal((await widerruf(koerper(), { ip: eigene })).status, 200, `Versuch ${i + 1}`)
|
||
const res = await widerruf(koerper(), { ip: eigene })
|
||
assert.equal(res.status, 429)
|
||
assert.equal(kopf(res, 'retry-after'), '3600')
|
||
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
|
||
assert.deepEqual(Object.keys(res.body), ['fehler'])
|
||
assert.match(res.body.fehler._, /Zu viele/)
|
||
})
|
||
|
||
test('Grenze je Shop (20 je Stunde): 429 für andere IPs', async () => {
|
||
for (let i = 0; i < 20; i++) assert.equal((await widerruf(koerper(), { ip: `198.51.100.${i + 1}` })).status, 200, `Versuch ${i + 1}`)
|
||
const res = await widerruf(koerper(), { ip: '198.51.100.99' })
|
||
assert.equal(res.status, 429)
|
||
assert.equal(res.body.fehler._.startsWith('Zu viele'), true)
|
||
})
|
||
|
||
test('Grenze für Anfragen je IP (darfAnfrage) kommt vor allem anderen: 429 mit Retry-After 60 und *', async () => {
|
||
const eigene = '203.0.113.88'
|
||
let letzte
|
||
for (let i = 0; i < 125; i++) {
|
||
letzte = await widerruf(koerper({ kontakt_url: 'x' }), { ip: eigene, origin: 'https://fremd.example' })
|
||
if (letzte.status === 429) break
|
||
}
|
||
assert.equal(letzte.status, 429)
|
||
assert.equal(kopf(letzte, 'retry-after'), '60')
|
||
assert.equal(kopf(letzte, 'access-control-allow-origin'), '*')
|
||
})
|
||
|
||
test('abgeschalteter Shop (disabled): Widerruf geht weiter', async () => {
|
||
await aufbau({ state: 'disabled' })
|
||
const res = await widerruf()
|
||
assert.equal(res.status, 200)
|
||
assert.equal(res.body.ok, true)
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 1)
|
||
assert.equal(outbox()[0].art, 'G12')
|
||
})
|
||
|
||
test('unbekannter siteKey, Modus off, Pilot ohne Treffer, andere Zustände: 200 { aktiv: false } ohne Origin-Prüfung', async () => {
|
||
const unbekannt = await post('/zzzzzzzzzzzzzzzz/widerruf', koerper(), { origin: 'https://fremd.example' })
|
||
assert.equal(unbekannt.status, 200)
|
||
assert.deepEqual(unbekannt.body, { aktiv: false })
|
||
assert.equal(kopf(unbekannt, 'access-control-allow-origin'), '*')
|
||
assert.equal(kopf(unbekannt, 'cache-control'), 'no-store')
|
||
const ungueltig = await post('/x/widerruf', koerper())
|
||
assert.deepEqual(ungueltig.body, { aktiv: false })
|
||
|
||
config.shop.mode = 'off'
|
||
assert.deepEqual((await widerruf()).body, { aktiv: false })
|
||
config.shop.mode = 'pilot'
|
||
config.shop.pilotEmails = ['jemand@anders.example']
|
||
assert.deepEqual((await widerruf()).body, { aktiv: false })
|
||
config.shop.pilotEmails = ['inhaber@brot.example']
|
||
assert.equal((await widerruf()).body.ok, true)
|
||
await warteAufVersand()
|
||
assert.equal(widerrufe().length, 1, 'nur der Pilot-Treffer')
|
||
|
||
await aufbau({ state: 'pending' })
|
||
assert.deepEqual((await widerruf()).body, { aktiv: false })
|
||
assert.equal(widerrufe().length, 0)
|
||
})
|
||
|
||
test('Mindestantwortzeit: auch Fehler und Fallen antworten nicht schneller als minAntwortMs', async () => {
|
||
config.shop.minAntwortMs = 250
|
||
for (const body of [koerper(), koerper({ kontakt_url: 'x' }), koerper({ formToken: 'kaputt' }), koerper({ name: '' })]) {
|
||
const t0 = performance.now()
|
||
await widerruf(body)
|
||
assert.ok(performance.now() - t0 >= 240, `zu schnell: ${Math.round(performance.now() - t0)} ms`)
|
||
}
|
||
})
|
||
|
||
test('Antworten enthalten nie IDs oder Adressen (Erfolg, Falle, Fehler)', async () => {
|
||
const antworten = [await widerruf(), await widerruf(koerper({ kontakt_url: 'x' })), await widerruf(koerper({ name: '' })), await widerruf(koerper({ formToken: 'x' }))]
|
||
for (const a of antworten) ohneInternes(a.body)
|
||
assert.deepEqual(Object.keys(antworten[0].body).sort(), ['eingang', 'ok'])
|
||
})
|