diff --git a/server/services/shop/widerrufe.js b/server/services/shop/widerrufe.js index 6034a65..263dbc1 100644 --- a/server/services/shop/widerrufe.js +++ b/server/services/shop/widerrufe.js @@ -1,7 +1,7 @@ import { config } from '../../config.js' import { createDocument, getDocument, listDocuments, Query, updateDocument } from '../appwriteAdmin.js' import { withLocks } from '../keyedLock.js' -import { darfWiderrufsversuch, zaehle } from '../termine/limits.js' +import { darfEmpfaenger, darfWiderrufsversuch, zaehle } from '../termine/limits.js' import { pruefeNow } from '../termine/util.js' import { datumKurz, uhrzeit } from '../termine/zeit.js' import { bestellDaten } from './bestellungen.js' @@ -147,8 +147,10 @@ export async function erfasseWiderruf(ctx, eingabe, { now = new Date(), ip } = { if (order) await verlaufErgaenzen(order.$id, now) // G12: Eingangsbestaetigung auf dauerhaftem Datentraeger, sofort; bestaetigungAt nur, wenn die Warteschlange sie angenommen hat + // Pro Empfaengeradresse hoechstens 3 je Stunde (gemeinsame Kappe der Funktionen): sonst liesse sich G12 als Mail-Relay an fremde + // Adressen missbrauchen. Der Widerruf ist gespeichert und beantwortet, die Bestaetigung entfaellt dann (bestaetigungAt bleibt leer). try { - const g12 = await anKaeufer('G12', ctx, order, { an: email, bookingId: doc.$id, name, bestellnummer, eingang: now, nachricht }, now) + const g12 = !darfEmpfaenger(email.trim().toLowerCase(), now) ? null : await anKaeufer('G12', ctx, order, { an: email, bookingId: doc.$id, name, bestellnummer, eingang: now, nachricht }, now) if (g12?.ok) await updateDocument(col(), doc.$id, { bestaetigungAt: now.toISOString() }) } catch (err) { console.warn(`[shop-widerrufe] G12 nicht bestätigt: ${err?.name}`) diff --git a/test/shopWiderrufe.test.js b/test/shopWiderrufe.test.js index 79d78cf..1498e31 100644 --- a/test/shopWiderrufe.test.js +++ b/test/shopWiderrufe.test.js @@ -94,6 +94,11 @@ afterEach(async () => { 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 @@ -257,6 +262,26 @@ test('Grenzen: 10 je IP und Stunde ⇒ 429, danach wieder frei; 20 je Shop und S 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 }))