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' // Webshop E5: Widerrufe (§ 356a BGB, services/shop/widerrufe.js). Mails landen in der Warteschlange (termineOutbox) bzw. der // Mailsenke; nichts verlässt den Prozess. const aw = await startAppwriteMock({ schema: { termineOutbox: schemaKeys('termineOutbox'), shopOrders: schemaKeys('shopOrders'), shopWiderrufe: schemaKeys('shopWiderrufe'), }, datetimes: { ...TERMINE_DATETIMES, ...SHOP_DATETIMES }, defaultLimit: 25, }) after(() => aw.close()) 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 { setMailTransportForTests } = await import('../server/services/mailer.js') const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js') const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js') const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js') const W = await import('../server/services/shop/widerrufe.js') const { normalisiereWiderruf, erfasseWiderruf, listeWiderrufe, markiereBearbeitet } = W after(() => setMailTransportForTests(null)) const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin (MESZ) const nach = (ms) => new Date(JETZT.getTime() + ms) const SITE_KEY = 'abcdefghijklmnop' const BASIS_VORHER = config.publicBaseUrl const ABSENDER_VORHER = config.smtp.from const ctxMit = (state = 'active', id = 'f_shop01') => ({ doc: { $id: id, customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: SITE_KEY, state }, shop: { id, customerId: 'c1', projectId: 'p1', siteKey: SITE_KEY, geoeffnet: false, einstellungen: { ...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)), kleinunternehmer: true, lieferzeit: '2–4 Werktage', shopname: 'Töpferei Muster', anbieter: { name: 'Erika Muster', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: 'DE', email: 'info@toepferei.example', telefon: '', ustId: '' }, }, rechtstexte: { entwurf: {}, bestaetigt: { texte: { agb: 'AGB', widerrufsbelehrung: 'Belehrung', widerrufsformular: 'Formular', impressumUrl: '', datenschutzUrl: '' }, hash: 'h1', at: JETZT.toISOString(), von: 'c1' } }, }, }) const EINGABE = { name: 'Max Mustermann', email: 'Max@Example.com', bestellnummer: '26-k7qm2x', nachricht: 'Ich widerrufe den Vertrag über die Tasse.' } const bestellung = (over = {}) => ({ $id: 'o_bestellung01', shopId: 'f_shop01', customerId: 'c1', bestellnummer: '26-K7QM2X', status: 'bezahlt', kaeuferEmail: 'max@example.com', summeCent: 1200, erstelltAt: JETZT.toISOString(), bezahltAt: JETZT.toISOString(), bestellung: JSON.stringify({ positionen: [{ name: 'Tasse', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }], verlauf: [{ at: JETZT.toISOString(), was: 'angelegt' }] }), ...over, }) let sink let log beforeEach(async () => { for (const sammlung of ['termineOutbox', 'customers', 'shopOrders', 'shopWiderrufe']) { for (const id of aw.docs(sammlung).keys()) await deleteDocument(config.collections[sammlung], id) } resetOutboxForTests() resetTermineLimitsForTests() config.smtp.from = 'noreply@webklar.com' config.publicBaseUrl = 'https://project.webklar.com' sink = mailSink() setMailTransportForTests(sink) log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) } aw.seed('customers', { $id: 'c1', name: 'Erika Muster', email: 'erika@toepferei.example' }) }) afterEach(async () => { await warteAufVersand() mock.restoreAll() config.smtp.from = ABSENDER_VORHER config.publicBaseUrl = BASIS_VORHER }) const widerrufe = () => [...aw.docs('shopWiderrufe').values()] const outbox = () => [...aw.docs('termineOutbox').values()] /** Zahl der G12 in der Warteschlange zu Widerrufen mit dieser E-Mail-Adresse (ohne passende Bestellung gilt die Widerruf-ID als bookingId) */ const g12An = (email) => { const ids = new Set(widerrufe().filter((d) => d.email === email).map((d) => d.$id)) return outbox().filter((e) => e.art === 'G12' && ids.has(e.bookingId)).length } const fehlerVon = async (promise) => { try { await promise } catch (err) { return err } assert.fail('hätte werfen müssen') } // ---------------------------------------------------------------- Prüfung test('normalisiereWiderruf: gültige Eingabe wird bereinigt (E-Mail klein, Bestellnummer groß, Steuerzeichen raus)', () => { const erg = normalisiereWiderruf({ name: ' Max \u0000 Mustermann\n', email: ' Max@Example.com ', bestellnummer: ' 26-k7qm2x ', nachricht: 'Zeile 1\r\nZeile\u0007 2\u0000 ' }) assert.deepEqual(erg, { widerruf: { name: 'Max Mustermann', email: 'max@example.com', bestellnummer: '26-K7QM2X', nachricht: 'Zeile 1\nZeile 2' } }) assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'X' }).widerruf.nachricht, '', 'Nachricht ist freiwillig') }) test('normalisiereWiderruf: Bestellnummer freiwillig; ohne Nummer ist die Erklärung Pflicht (§ 356a BGB)', () => { const ohneNummer = normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: ' ', nachricht: 'Bestellt am 01.10., Tasse blau' }) assert.deepEqual(ohneNummer, { widerruf: { name: 'Ab', email: 'a@b.de', bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' } }) assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.') assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', nachricht: ' \n ' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.') assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '26-K7QM2X' }).fehler, undefined, 'mit Nummer bleibt die Erklärung freiwillig') assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'a b', nachricht: 'x' }).fehler.bestellnummer, 'gegebene Nummer: gleiche Formatregel') }) test('erfasseWiderruf ohne Bestellnummer: gespeichert ohne Zuordnung, G12 und K26 mit „–“', async () => { aw.seed('shopOrders', bestellung()) const erg = await erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' }, { now: JETZT, ip: '203.0.113.77' }) assert.equal(erg.ok, true) const d = widerrufe().find((x) => x.$id === erg.id) assert.equal(d.bestellnummer, '') assert.equal(d.orderId, '') await warteAufVersand() assert.equal(outbox().filter((e) => e.art === 'G12').length, 1) assert.equal(JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf.length, 1, 'ohne Nummer keine Zuordnung zum Verlauf') await assert.rejects( () => erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: '' }, { now: JETZT, ip: '203.0.113.78' }), (err) => err.status === 400 && err.fehler.nachricht === 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.', ) }) test('normalisiereWiderruf: Fehler je Feld in Sie-Form', () => { const grenzen = normalisiereWiderruf({ name: 'M', email: 'keine-adresse', bestellnummer: '', nachricht: 'x'.repeat(2001) }) assert.deepEqual(Object.keys(grenzen.fehler).sort(), ['email', 'nachricht', 'name'], 'leere Bestellnummer ist erlaubt') for (const text of Object.values(grenzen.fehler)) assert.match(text, /Bitte|darf/) assert.equal(normalisiereWiderruf({ name: 'x'.repeat(201), email: 'a@b.de', bestellnummer: '1' }).fehler.name.includes('200'), true) assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'x'.repeat(33) }).fehler.bestellnummer) assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '26_K7 QM' }).fehler.bestellnummer, 'nur Buchstaben, Ziffern, Bindestrich') assert.ok(normalisiereWiderruf({ name: 'Ab', email: `${'a'.repeat(250)}@b.de`, bestellnummer: '1' }).fehler.email, 'über 254 Zeichen') assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '1', nachricht: 'x'.repeat(2000) }).fehler, undefined) // mehrere Adressen / Namensform / eingeschmuggelter Zeilenumbruch for (const email of ['a@b.de, c@d.de', 'a@b.de;c@d.de', 'Max ', 'a@b.de\nc@d.de', 'a b@c.de', 'a@b']) { assert.ok(normalisiereWiderruf({ name: 'Ab', email, bestellnummer: '1' }).fehler?.email, email) } assert.deepEqual(Object.keys(normalisiereWiderruf(null).fehler).sort(), ['email', 'nachricht', 'name']) assert.ok(normalisiereWiderruf({ name: 5, email: {}, bestellnummer: [] }).fehler.name, 'falsche Typen sind leer') }) // ---------------------------------------------------------------- Erfassen test('passende Bestellung: orderId gesetzt, Verlauf ergänzt, G12 in der Warteschlange mit Datum und Uhrzeit, K26 an den Händler', async () => { aw.seed('shopOrders', bestellung()) const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' }) await warteAufVersand() assert.equal(erg.ok, true) assert.equal(erg.eingang, '05.10.2026, 14:03 Uhr') const [doc] = widerrufe() assert.equal(erg.id, doc.$id) assert.deepEqual( { shopId: doc.shopId, customerId: doc.customerId, bestellnummer: doc.bestellnummer, name: doc.name, email: doc.email, orderId: doc.orderId }, { shopId: 'f_shop01', customerId: 'c1', bestellnummer: '26-K7QM2X', name: 'Max Mustermann', email: 'max@example.com', orderId: 'o_bestellung01' }, ) assert.equal(doc.nachricht, EINGABE.nachricht) assert.equal(new Date(doc.eingangAt).getTime(), JETZT.getTime()) assert.equal(new Date(doc.bestaetigungAt).getTime(), JETZT.getTime(), 'Warteschlange hat G12 angenommen') const verlauf = JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf assert.deepEqual(verlauf.map((v) => v.was), ['angelegt', 'widerruf']) assert.equal(verlauf[1].at, JETZT.toISOString()) // G12 an den Käufer (Warteschlange): bookingId = Bestell-ID (mails.js bevorzugt sie vor der Widerruf-ID), Absender Shopname, Antwort an den Händler const [g12] = outbox() assert.equal(g12.art, 'G12') assert.equal(g12.featureId, 'f_shop01') assert.equal(g12.bookingId, 'o_bestellung01') assert.equal(g12.fromName, 'Töpferei Muster') assert.equal(g12.replyTo, 'info@toepferei.example') const kaeufer = sink.sent.find((m) => m.to.some((t) => t.address === 'max@example.com')) assert.ok(kaeufer, 'G12 versandt') for (const teil of ['26-K7QM2X', 'Max Mustermann', '05.10.2026, 14:03 Uhr', 'Ich widerrufe den Vertrag über die Tasse.']) assert.ok(kaeufer.text.includes(teil), teil) // K26 an den Händler, ohne BCC const haendler = sink.sent.find((m) => m.to.some((t) => t.address === 'erika@toepferei.example')) assert.ok(haendler, 'K26 versandt') assert.ok(!haendler.bcc) for (const teil of ['Max Mustermann', 'max@example.com', '26-K7QM2X', '05.10.2026, 14:03 Uhr']) assert.ok(haendler.text.includes(teil), teil) assert.equal(sink.sent.length, 2) }) test('keine passende Bestellung (falsche Nummer, fremde E-Mail, anderer Shop): trotzdem gespeichert und bestätigt, ohne orderId und ohne Verlauf', async () => { aw.seed('shopOrders', bestellung()) aw.seed('shopOrders', bestellung({ $id: 'o_fremd', shopId: 'f_anderer', bestellnummer: '26-FREMD1', kaeuferEmail: 'max@example.com' })) const faelle = [ { bestellnummer: '26-ZZZZZZ' }, { email: 'jemand@example.com' }, { bestellnummer: '26-FREMD1' }, // existiert nur in einem anderen Shop ] for (const [i, abweichung] of faelle.entries()) { const erg = await erfasseWiderruf(ctxMit(), { ...EINGABE, ...abweichung }, { now: nach(i * 1000), ip: `203.0.113.${i + 10}` }) assert.equal(erg.ok, true) assert.equal(widerrufe().find((d) => d.$id === erg.id).orderId, '') } await warteAufVersand() assert.equal(JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf.length, 1) assert.equal(JSON.parse(aw.docs('shopOrders').get('o_fremd').bestellung).verlauf.length, 1) assert.equal(outbox().length, 3) assert.ok(outbox().every((e) => e.art === 'G12')) assert.deepEqual(outbox().map((e) => e.bookingId).sort(), widerrufe().map((d) => d.$id).sort(), 'ohne Bestellung gilt die Widerruf-ID') assert.equal(widerrufe().every((d) => d.bestaetigungAt), true, 'Bestätigung auch ohne passende Bestellung') }) test('E-Mail-Vergleich ohne Beachtung der Groß-/Kleinschreibung; anonymisierte Bestellung (leere E-Mail) passt nie', async () => { aw.seed('shopOrders', bestellung({ kaeuferEmail: 'MAX@Example.COM' })) const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' }) assert.equal(widerrufe().find((d) => d.$id === erg.id).orderId, 'o_bestellung01') await deleteDocument(config.collections.shopOrders, 'o_bestellung01') aw.seed('shopOrders', bestellung({ kaeuferEmail: '' })) const erg2 = await erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'x@example.com' }, { now: nach(1000), ip: '203.0.113.5' }) assert.equal(widerrufe().find((d) => d.$id === erg2.id).orderId, '') }) test('ungültige Eingabe: Fehler 400 mit Feldtexten, nichts gespeichert, keine Mail', async () => { const err = await fehlerVon(erfasseWiderruf(ctxMit(), { ...EINGABE, name: '' }, { now: JETZT, ip: '203.0.113.5' })) assert.equal(err.status, 400) assert.ok(err.fehler.name) assert.equal(widerrufe().length, 0) assert.equal(outbox().length, 0) }) test('Grenzen: 10 je IP und Stunde ⇒ 429, danach wieder frei; 20 je Shop und Stunde; andere Shops unberührt', async () => { const ip = '203.0.113.7' for (let i = 0; i < 10; i += 1) await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(i * 1000), ip }) const err = await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: nach(11000), ip })) assert.equal(err.status, 429) assert.equal(err.message, 'Zu viele Versuche. Bitte versuchen Sie es später noch einmal.') assert.equal(widerrufe().length, 10) // eine andere IP geht noch await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(12000), ip: '203.0.113.8' }) // nach einer Stunde ist die IP wieder frei await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(61 * 60 * 1000), ip }) // 20 je Shop: bisher 12 Versuche in der ersten Stunde (11 + 1 in der zweiten) ⇒ in der zweiten Stunde weiter mit wechselnden IPs resetTermineLimitsForTests() const t = nach(3 * 60 * 60 * 1000) for (let i = 0; i < 20; i += 1) await erfasseWiderruf(ctxMit(), EINGABE, { now: t, ip: `198.51.100.${i + 1}` }) const shopErr = await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: t, ip: '198.51.100.99' })) assert.equal(shopErr.status, 429) await erfasseWiderruf(ctxMit('active', 'f_shop02'), EINGABE, { now: t, ip: '198.51.100.99' }) }) test('M3: G12 geht je Empfängeradresse höchstens 3 Mal pro Stunde raus; der Widerruf wird trotzdem gespeichert und mit eingang beantwortet', async () => { const ziel = { ...EINGABE, email: 'opfer@example.com' } const antworten = [] for (let i = 0; i < 5; i += 1) antworten.push(await erfasseWiderruf(ctxMit(), ziel, { now: nach(i * 1000), ip: `203.0.113.${100 + i}` })) assert.ok(antworten.every((a) => a.ok === true && typeof a.eingang === 'string' && a.eingang !== '')) assert.equal(widerrufe().filter((d) => d.email === 'opfer@example.com').length, 5, 'alle Widerrufe gespeichert') await warteAufVersand() assert.equal(g12An('opfer@example.com'), 3) const mitBestaetigung = widerrufe().filter((d) => d.email === 'opfer@example.com' && d.bestaetigungAt).length assert.equal(mitBestaetigung, 3) // eine andere Adresse ist nicht betroffen await erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'andere@example.com' }, { now: nach(6000), ip: '203.0.113.120' }) await warteAufVersand() assert.equal(g12An('andere@example.com'), 1) // nach einer Stunde geht wieder eine Bestaetigung raus await erfasseWiderruf(ctxMit(), ziel, { now: nach(61 * 60 * 1000), ip: '203.0.113.130' }) await warteAufVersand() assert.equal(g12An('opfer@example.com'), 4) }) test('ein ungültiger Versuch zählt gegen die Grenze (kein Raten ohne Limit)', async () => { const ip = '203.0.113.9' for (let i = 0; i < 10; i += 1) await fehlerVon(erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'x' }, { now: JETZT, ip })) assert.equal((await fehlerVon(erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip }))).status, 429) }) test('Mailfehler kippen die Anfrage nicht: Warteschlange verweigert G12 ⇒ ok, bestaetigungAt leer; K26-Versand scheitert ⇒ ok', async () => { // G12: die Warteschlange verweigert den Eintrag (fehlende featureId) const ohneFeature = ctxMit() ohneFeature.doc.$id = '' const erg = await erfasseWiderruf(ohneFeature, EINGABE, { now: JETZT, ip: '203.0.113.5' }) assert.equal(erg.ok, true) assert.equal(erg.eingang, '05.10.2026, 14:03 Uhr') const doc = widerrufe().find((d) => d.$id === erg.id) assert.equal(doc.bestaetigungAt ?? '', '', 'nicht bestätigt, weil nichts eingereiht wurde') assert.equal(outbox().length, 0) await warteAufVersand() // K26: SMTP wirft setMailTransportForTests({ sendMail: async () => Promise.reject(Object.assign(new Error('smtp: max@example.com abgelehnt'), { code: 'ECONNECTION' })) }) const erg2 = await erfasseWiderruf(ctxMit(), EINGABE, { now: nach(1000), ip: '203.0.113.5' }) assert.equal(erg2.ok, true) await warteAufVersand() assert.ok(widerrufe().find((d) => d.$id === erg2.id).bestaetigungAt, 'G12 war eingereiht') assert.ok(!log.warn.mock.calls.some((a) => a.arguments.map(String).join(' ').includes('max@example.com')), 'keine Adresse im Log') }) test('K26 ohne Kunden-E-Mail scheitert still, Widerruf und G12 bleiben', async () => { for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id) const erg = await erfasseWiderruf(ctxMit(), EINGABE, { now: JETZT, ip: '203.0.113.5' }) assert.equal(erg.ok, true) await warteAufVersand() assert.equal(outbox().length, 1) }) test('geschlossener oder deaktivierter Shop: Widerruf wird trotzdem angenommen', async () => { for (const [i, state] of ['active', 'disabled'].entries()) { const ctx = ctxMit(state) ctx.shop.geoeffnet = false const erg = await erfasseWiderruf(ctx, EINGABE, { now: nach(i * 1000), ip: `203.0.113.${20 + i}` }) assert.equal(erg.ok, true, state) } await warteAufVersand() assert.equal(widerrufe().length, 2) assert.equal(outbox().length, 2) }) // ---------------------------------------------------------------- Verwalten test('listeWiderrufe: neueste zuerst, 50 je Seite, nur der eigene Shop', async () => { for (let i = 0; i < 53; i += 1) { aw.seed('shopWiderrufe', { $id: `w_${String(i).padStart(3, '0')}`, shopId: 'f_shop01', customerId: 'c1', bestellnummer: `26-N${i}`, name: 'Max', email: 'max@example.com', nachricht: '', eingangAt: nach(i * 60000).toISOString() }) } aw.seed('shopWiderrufe', { $id: 'w_fremd', shopId: 'f_shop02', customerId: 'c2', bestellnummer: '26-X', name: 'Fremd', email: 'f@example.com', nachricht: '', eingangAt: nach(999 * 60000).toISOString() }) const s1 = await listeWiderrufe('f_shop01') assert.equal(s1.widerrufe.length, 50) assert.equal(s1.mehr, true) assert.equal(s1.seite, 1) assert.equal(s1.widerrufe[0].id, 'w_052') assert.equal(s1.widerrufe[49].id, 'w_003') assert.ok(!s1.widerrufe.some((w) => w.id === 'w_fremd')) const s2 = await listeWiderrufe('f_shop01', { seite: 2 }) assert.deepEqual(s2.widerrufe.map((w) => w.id), ['w_002', 'w_001', 'w_000']) assert.equal(s2.mehr, false) assert.deepEqual((await listeWiderrufe('f_shop01', { seite: 3 })).widerrufe, []) assert.deepEqual((await listeWiderrufe('f_leer')).widerrufe, []) }) test('markiereBearbeitet: setzt bearbeitetAt einmal; fremder Shop oder unbekannte ID ⇒ 404 und keine Änderung', async () => { aw.seed('shopWiderrufe', { $id: 'w_eigen', shopId: 'f_shop01', customerId: 'c1', bestellnummer: '26-A', name: 'Max', email: 'max@example.com', nachricht: '', eingangAt: JETZT.toISOString() }) aw.seed('shopWiderrufe', { $id: 'w_fremd', shopId: 'f_shop02', customerId: 'c2', bestellnummer: '26-B', name: 'Fremd', email: 'f@example.com', nachricht: '', eingangAt: JETZT.toISOString() }) const fremd = await fehlerVon(markiereBearbeitet('f_shop01', 'w_fremd', { now: nach(1000) })) assert.equal(fremd.status, 404) assert.ok(!aw.docs('shopWiderrufe').get('w_fremd').bearbeitetAt) for (const id of ['w_gibtsnicht', '../x', '', null]) assert.equal((await fehlerVon(markiereBearbeitet('f_shop01', id, { now: nach(1000) }))).status, 404, String(id)) const erg = await markiereBearbeitet('f_shop01', 'w_eigen', { now: nach(2000) }) assert.equal(erg.id, 'w_eigen') assert.equal(new Date(erg.bearbeitetAt).getTime(), nach(2000).getTime()) const nochmal = await markiereBearbeitet('f_shop01', 'w_eigen', { now: nach(9000) }) assert.equal(new Date(nochmal.bearbeitetAt).getTime(), nach(2000).getTime(), 'zweiter Aufruf ändert nichts') assert.equal((await listeWiderrufe('f_shop01')).widerrufe[0].bearbeitetAt, erg.bearbeitetAt) })