Terminplaner: Empfaenger-Kappe ueber Betriebe (G01, Freitext in G04/G06)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c35711caad
commit
4d96565240
5 files changed
+257
-18
No files matched your search
@@ -13,8 +13,11 @@ import { pruefeNow, pruefeText } from './util.js'
|
||||
* Buchung, ein Treffer bekommt seine Mails immer.
|
||||
* Oeffentliche Schnittstelle (routes/publicTermine.js): darfAnfrage / darfAnfrageSite (lesende Abrufe und Token-Aufrufe
|
||||
* je IP und je siteKey), darfBuchungsversuch (je IP), darfHaltung (je siteKey), darfWiderrufsversuch (je IP) und darfG01
|
||||
* (Bestaetigungsmails je Adresse und Funktion). IPv6-Adressen zaehlen je /64 (ipSchluessel): ein Anschluss bekommt meist
|
||||
* (Bestaetigungsmails je Adresse). IPv6-Adressen zaehlen je /64 (ipSchluessel): ein Anschluss bekommt meist
|
||||
* ein ganzes /64, die Adresse darin wechselt beliebig.
|
||||
* Empfaenger-Kappe: eine fremde Adresse bekommt ueber alle Betriebe (Funktionen) zusammen hoechstens 3 Mails je Stunde und
|
||||
* 10 je Tag, die ein Gast oder ein Betrieb ausloest (darfG01 beim Gast-Formular, darfEmpfaenger fuer den Freitext des
|
||||
* Betriebs in G04/G06, mails.js anGast); beide teilen dieselben Zaehler.
|
||||
*
|
||||
* Speicher: zwei getrennte Maps mit je hoechstens 10 000 Schluesseln. Die Zaehler je IP (Schluessel mit 'ip:') haben
|
||||
* beliebig viele, von Angreifern frei waehlbare Schluessel; sie duerfen die langlebigen Zaehler je Adresse, Funktion
|
||||
@@ -46,6 +49,9 @@ const HALTUNGEN_JE_SITE = 25
|
||||
const WIDERRUFE_JE_IP = 10
|
||||
/** G01 (Bestaetigungsmail, auch "zu viele"): je Adresse und Funktion und Tag. */
|
||||
const G01_JE_ADRESSE = 5
|
||||
/** Empfaenger-Kappe ueber alle Funktionen: je (normalisierter) Adresse und Stunde, je Adresse und Tag. */
|
||||
const EMPFAENGER_JE_STUNDE = 3
|
||||
const EMPFAENGER_JE_TAG = 10
|
||||
|
||||
/**
|
||||
* Eine Map schluessel -> { start, ende (ms), n }; die Einfuegereihenfolge ist die Reihenfolge der Fensterbeginne.
|
||||
@@ -195,9 +201,27 @@ export function darfWiderrufsversuch(ip, now) {
|
||||
return zaehle(`ip:widerruf:${ipSchluessel(ip)}`, WIDERRUFE_JE_IP, STUNDE_MS, now)
|
||||
}
|
||||
|
||||
/** Darf an diese (normalisierte) Adresse eine G01 gehen? Hoechstens 5 je Adresse und Funktion und Tag (gehalten wie "zu viele"). */
|
||||
/** Die zwei Regeln der Empfaenger-Kappe (Stunde und Tag) fuer eine normalisierte Adresse, ueber alle Funktionen. */
|
||||
const empfaengerRegeln = (adresse) => [
|
||||
{ schluessel: `empfaenger-h:${adresse}`, max: EMPFAENGER_JE_STUNDE, fensterMs: STUNDE_MS },
|
||||
{ schluessel: `empfaenger-d:${adresse}`, max: EMPFAENGER_JE_TAG, fensterMs: TAG_MS },
|
||||
]
|
||||
|
||||
/**
|
||||
* Darf an diese (normalisierte: klein, getrimmt) Adresse eine G01 gehen? Hoechstens 5 je Adresse und Funktion und Tag
|
||||
* (gehalten wie "zu viele") und die Empfaenger-Kappe ueber alle Funktionen (3 je Stunde, 10 je Tag). Alles oder nichts:
|
||||
* eine abgelehnte Anfrage erhoeht keinen Zaehler.
|
||||
*/
|
||||
export function darfG01(featureId, adresse, now) {
|
||||
return zaehle(`g01:${featureId}:${adresse}`, G01_JE_ADRESSE, TAG_MS, now)
|
||||
return zaehleAlle([{ schluessel: `g01:${featureId}:${adresse}`, max: G01_JE_ADRESSE, fensterMs: TAG_MS }, ...empfaengerRegeln(adresse)], now)
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf an diese (normalisierte) Adresse eine Mail mit Freitext des Betriebs gehen (G04, G06)? Dieselben Zaehler wie
|
||||
* darfG01 (3 je Stunde, 10 je Tag ueber alle Funktionen), aber ohne die Regel je Funktion.
|
||||
*/
|
||||
export function darfEmpfaenger(adresse, now) {
|
||||
return zaehleAlle(empfaengerRegeln(adresse), now)
|
||||
}
|
||||
|
||||
/** Leert alle Zaehler (Tests). */
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getDocument } from '../appwriteAdmin.js'
|
||||
import { preisHinweis, preisText, zahlungText } from './einstellungen.js'
|
||||
import { icsTermin } from './ics.js'
|
||||
import { websiteUrl } from './kontext.js'
|
||||
import { darfEmpfaenger } from './limits.js'
|
||||
import { einreihen } from './outbox.js'
|
||||
import { widerrufsbelehrung } from './rechtstexte.js'
|
||||
import { buchungsNummer, verwaltungsToken } from './token.js'
|
||||
@@ -21,6 +22,8 @@ import { datumKurz, datumLang, uhrzeit } from './zeit.js'
|
||||
* - Person oder Platz (Block person in G02/G05) nur, wenn der Betrieb mehrere Ressourcen hat; K20/K21 zeigen sie immer.
|
||||
* - G01 und G08 enthalten keinen Text, den der Gast eingegeben hat (die Mail geht an eine ungeprueft angegebene Adresse);
|
||||
* in keinem Betreff steht ein Gastname.
|
||||
* - Freitext des Betriebs (G04 nachricht, G06 grund und nachricht bei Absage durch den Betrieb) geht nur bis zur
|
||||
* Empfaenger-Kappe (limits.js darfEmpfaenger) an eine Adresse; danach geht die Mail ohne diesen Text raus (anGast).
|
||||
* - Verwaltungslink `${publicBaseUrl}/termin.html#m=<verwaltungsToken>` (G02, G03, G05, G07), Bestaetigungslink `#v=<token>`
|
||||
* (G01, Token von extra.verifyToken), Portal-Link der Betriebsmails `${publicBaseUrl}/dashboard.html#termine`.
|
||||
*
|
||||
@@ -52,6 +55,8 @@ const GAST_ARTEN = ['G01', 'G02', 'G03', 'G04', 'G05', 'G06', 'G07', 'G08']
|
||||
const BETRIEBS_ARTEN = ['K20', 'K21']
|
||||
const ICS_METHODE = { G02: 'REQUEST', G07: 'REQUEST', G06: 'CANCEL', K20: 'PUBLISH' }
|
||||
const VERIFY_TOKEN_RE = /^[A-Za-z0-9_-]{43}$/
|
||||
/** Felder mit Freitext des Betriebs je Gastmail (G06 nur bei von === 'betrieb'). */
|
||||
const FREITEXT_FELDER = { G04: ['nachricht'], G06: ['grund', 'nachricht'] }
|
||||
|
||||
function einfrieren(wert) {
|
||||
if (wert && typeof wert === 'object' && !Object.isFrozen(wert)) {
|
||||
@@ -458,10 +463,26 @@ function kalenderDatei(art, ctx, buchung, extra, jetzt) {
|
||||
|
||||
// ---------------------------------------------------------------- Einreihen
|
||||
|
||||
/**
|
||||
* Zaehlt eine Mail mit Freitext des Betriebs gegen die Empfaenger-Kappe der Adresse (limits.js darfEmpfaenger) und gibt
|
||||
* das extra zurueck, mit dem die Mail rausgeht: unveraendert, solange die Kappe nicht erreicht ist oder kein Freitext
|
||||
* dabei ist (dann zaehlt nichts), sonst ohne die Freitextfelder (Log-Warnung ohne Adresse). G05, K20/K21 und der Rest
|
||||
* sind nicht betroffen, ebenso G06 des Gastes oder des Systems.
|
||||
*/
|
||||
function begrenzeFreitext(art, extra, empfaenger, now) {
|
||||
const felder = FREITEXT_FELDER[art]
|
||||
if (!felder || (art === 'G06' && extra.von !== 'betrieb') || !felder.some((feld) => hatText(extra[feld]))) return extra
|
||||
const adresse = hatText(empfaenger) ? empfaenger.trim().toLowerCase() : ''
|
||||
if (adresse === '' || darfEmpfaenger(adresse, now)) return extra
|
||||
console.warn(`[termine-mails] ${art}: Empfänger-Kappe erreicht, die Mail geht ohne Freitext raus`)
|
||||
return { ...extra, ...Object.fromEntries(felder.map((feld) => [feld, undefined])) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reiht eine Mail an den Gast ein (Absender: Betriebsname, Antworten an die Betriebs-E-Mail, nie BCC). Empfaenger ist
|
||||
* extra.an, sonst buchung.gastEmail. G02 und G07 tragen eine Einladung (REQUEST), G06 die Absage (CANCEL), jeweils
|
||||
* ausser mit extra.ohneIcs.
|
||||
* ausser mit extra.ohneIcs. Freitext des Betriebs in G04/G06 geht nur bis zur Empfaenger-Kappe der Adresse mit (siehe
|
||||
* begrenzeFreitext).
|
||||
* @param {string} art 'G01' ... 'G08'
|
||||
* @param {object} ctx
|
||||
* @param {object|null} buchung
|
||||
@@ -472,7 +493,8 @@ function kalenderDatei(art, ctx, buchung, extra, jetzt) {
|
||||
export async function anGast(art, ctx, buchung, extra = {}, now = new Date()) {
|
||||
if (!GAST_ARTEN.includes(art)) throw new TypeError(`anGast: ${String(art).slice(0, 20)} ist keine Gastmail`)
|
||||
pruefeNow(now, 'anGast')
|
||||
extra = extra ?? {}
|
||||
const an = hatText(extra?.an) ? extra.an : buchung?.gastEmail
|
||||
extra = begrenzeFreitext(art, extra ?? {}, an, now)
|
||||
const { values, blocks } = werteFuer(art, ctx, buchung, extra)
|
||||
const b = ctx.einstellung.betrieb
|
||||
const ics = extra.ohneIcs === true ? null : kalenderDatei(art, ctx, buchung, extra, now)
|
||||
@@ -481,7 +503,7 @@ export async function anGast(art, ctx, buchung, extra = {}, now = new Date()) {
|
||||
featureId: ctx.feature?.$id,
|
||||
bookingId: buchung?.$id || extra.bookingId || OHNE_BUCHUNG,
|
||||
art,
|
||||
an: hatText(extra.an) ? extra.an : buchung?.gastEmail,
|
||||
an,
|
||||
fromName: b.name,
|
||||
replyTo: b.email,
|
||||
template: art,
|
||||
|
||||
@@ -566,7 +566,10 @@ test('Buchung: 201 mit maskierter Adresse und Haltezeit; Buchung gehalten, Mail
|
||||
|
||||
test('zu viele offene Termine: dieselbe 201-Antwort, keine neue Buchung, statt der Bestaetigung geht G01 "zu viele"', async () => {
|
||||
await speichere()
|
||||
for (const zeit of ['10:00', '11:00', '12:00']) assert.equal((await buchen({ zeit })).status, 201)
|
||||
// drei bestaetigte Termine belegen die drei offenen Plaetze; die Kappe der Adresse (3 Mails je Stunde) ist damit ausgeschoepft,
|
||||
// darum kommt die vierte Anfrage erst eine Stunde spaeter bis zu "zu viele" (in der Stunde selbst bliebe sie still, siehe oben)
|
||||
for (const zeit of ['10:00', '11:00', '12:00']) await gebucht({ zeit })
|
||||
setzeUhrFuerTests(() => plus(JETZT, 61 * 60000))
|
||||
await warteAufVersand()
|
||||
const vorher = sink.sent.length
|
||||
const vierte = await buchen({ zeit: '13:00' })
|
||||
@@ -627,7 +630,7 @@ test('je siteKey 25 gehaltene Termine pro Stunde: danach 429, ohne dass eine Buc
|
||||
assert.equal(buchungen().length, 1)
|
||||
})
|
||||
|
||||
test('je Adresse und Funktion 5 Bestaetigungsmails pro Tag: die sechste Anfrage bekommt dieselbe Antwort, aber keine Mail', async () => {
|
||||
test('je Adresse 3 Bestaetigungsmails pro Stunde: die vierte bis sechste Anfrage bekommt dieselbe Antwort, aber keine Haltung und keine Mail', async () => {
|
||||
await speichere()
|
||||
const zeiten = ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00']
|
||||
const antworten = []
|
||||
@@ -637,7 +640,7 @@ test('je Adresse und Funktion 5 Bestaetigungsmails pro Tag: die sechste Anfrage
|
||||
assert.ok(antworten.every((a) => JSON.stringify(a.body) === JSON.stringify(antworten[0].body)), 'alle Antworten gleich')
|
||||
assert.deepEqual(antworten[0].body, { ok: true, email: 'v***@example.com', minuten: 30 })
|
||||
assert.equal(buchungen().length, 3, 'nur drei offene Termine')
|
||||
assert.equal(mailsAn('viel@example.com').length, 5, '3 Bestaetigungen + 2 "zu viele", die sechste Anfrage bekommt nichts')
|
||||
assert.equal(mailsAn('viel@example.com').length, 3, 'drei Bestaetigungen; die vierte bis sechste Anfrage bekommt nichts (auch keine "zu viele")')
|
||||
|
||||
// andere Adressen zaehlen getrennt; Fehler der Zeit kommen vor der Mailgrenze (kein Orakel, ob die Adresse an der Grenze ist)
|
||||
const andere = await buchen({ zeit: '16:00', email: 'anna@example.com' })
|
||||
|
||||
@@ -31,7 +31,8 @@ const { resetOutboxForTests, warteAufVersand } = await import('../server/service
|
||||
const { hashToken, verwaltungsToken, buchungsNummer } = await import('../server/services/termine/token.js')
|
||||
const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||
const { zellenFuer, zellenId } = await import('../server/services/termine/zellen.js')
|
||||
const { resetTermineLimitsForTests } = await import('../server/services/termine/limits.js')
|
||||
const { resetTermineLimitsForTests, darfEmpfaenger } = await import('../server/services/termine/limits.js')
|
||||
const { anGast } = await import('../server/services/termine/mails.js')
|
||||
const { halte, vorschau, bestaetige, kannWiderrufen, verschiebeGast } = await import('../server/services/termine/ablauf.js')
|
||||
const { aktion, manuell, sperre, entsperre } = await import('../server/services/termine/ablaufBetrieb.js')
|
||||
after(() => setMailTransportForTests(null))
|
||||
@@ -395,6 +396,132 @@ test('absagen: eine Anfrage (nie eine Einladung erhalten) bekommt G06 ohne Kalen
|
||||
assert.equal(err.code, 'nicht_moeglich')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Empfaenger-Kappe fuer Freitext (G04, G06)
|
||||
|
||||
const GAST = 'max@example.com'
|
||||
/** Alle Warnungen und Fehler im Log als ein Text (die Adresse des Gastes darf dort nie stehen). */
|
||||
const logText = () => JSON.stringify([...log.warn.mock.calls, ...log.error.mock.calls].map((c) => c.arguments.map((a) => String(a?.message ?? a))))
|
||||
/** Verbraucht n Platz(e) der Empfaenger-Kappe einer Adresse (wie n Mails mit Freitext oder G01 davor). */
|
||||
const fuelleKappe = (adresse, n, now = JETZT) => {
|
||||
for (let i = 0; i < n; i += 1) assert.equal(darfEmpfaenger(adresse, now), true)
|
||||
}
|
||||
/** Reiht eine Gastmail direkt ein und liefert die versandte Mail. */
|
||||
async function sende(art, ctx, buchungsDoc, extra, now = JETZT) {
|
||||
const vorher = sink.sent.length
|
||||
await anGast(art, ctx, buchungsDoc, extra, now)
|
||||
await warteAufVersand()
|
||||
assert.equal(sink.sent.length, vorher + 1)
|
||||
return sink.sent[vorher]
|
||||
}
|
||||
|
||||
test('ablehnen: ueber der Kappe (3 je Adresse und Stunde) geht G04 ohne die Nachricht des Betriebs raus, darunter mit; die Warnung im Log nennt keine Adresse', async () => {
|
||||
const ctx = await speichere()
|
||||
const a = await gebucht(ctx, { leistung: S_ANFRAGE })
|
||||
const b = await gebucht(ctx, { leistung: S_ANFRAGE, zeit: '12:00' })
|
||||
fuelleKappe(GAST, 2) // zwei Mails mit Freitext sind schon raus: die dritte darf noch
|
||||
const vorher = sink.sent.length
|
||||
|
||||
await aktion(ctx, a.id, { aktion: 'ablehnen', nachricht: 'Am Montag sind wir im Urlaub.' }, JETZT)
|
||||
await warteAufVersand()
|
||||
assert.match(sink.sent[vorher].text, /Am Montag sind wir im Urlaub\./, 'die dritte Mail der Stunde enthaelt den Text')
|
||||
assert.doesNotMatch(logText(), /Kappe/)
|
||||
|
||||
await aktion(ctx, b.id, { aktion: 'ablehnen', nachricht: 'Bitte ruft uns einfach an.' }, JETZT)
|
||||
await warteAufVersand()
|
||||
assert.equal(sink.sent.length, vorher + 2, 'die Mail geht trotzdem raus')
|
||||
const mail = sink.sent[vorher + 1]
|
||||
assert.equal(mail.to[0].address, GAST)
|
||||
assert.equal(mail.subject, 'Ihre Terminanfrage bei Salon Müller – leider abgelehnt')
|
||||
assert.match(mail.text, /leider kann Salon Müller Ihre Terminanfrage nicht annehmen/, 'der Hinweis auf die Ablehnung bleibt')
|
||||
assert.doesNotMatch(mail.text, /Bitte ruft uns einfach an/, 'der Freitext faellt weg')
|
||||
assert.doesNotMatch(mail.text, /Nachricht von/i)
|
||||
assert.doesNotMatch(JSON.stringify(eintraege()), /Bitte ruft uns einfach an/, 'er steht auch nicht im Postausgang')
|
||||
assert.equal(buchung(b.id).status, 'abgelehnt')
|
||||
|
||||
assert.match(logText(), /G04: Empfänger-Kappe erreicht/)
|
||||
assert.doesNotMatch(logText(), /max@example\.com|example\.com/, 'das Log nennt keine Adresse')
|
||||
})
|
||||
|
||||
test('absagen: ueber der Kappe geht G06 des Betriebs ohne Grund und Nachricht raus (Kalenderabsage und Hinweis bleiben); das Log nennt keine Adresse', async () => {
|
||||
const ctx = await speichere()
|
||||
const { id } = await gebucht(ctx)
|
||||
fuelleKappe(GAST, 3)
|
||||
const vorher = sink.sent.length
|
||||
|
||||
assert.deepEqual(
|
||||
await aktion(ctx, id, { aktion: 'absagen', grund: 'Krankheit', nachricht: 'Gute Besserung wuenschen wir.' }, JETZT),
|
||||
{ status: 'abgesagt' },
|
||||
)
|
||||
await warteAufVersand()
|
||||
|
||||
assert.equal(sink.sent.length, vorher + 1)
|
||||
const g06 = sink.sent[vorher]
|
||||
assert.equal(g06.to[0].address, GAST)
|
||||
assert.equal(g06.subject, 'Ihr Termin am 26.10.2026 wurde abgesagt')
|
||||
assert.match(g06.text, /Salon Müller/)
|
||||
assert.doesNotMatch(g06.text, /Krankheit/)
|
||||
assert.doesNotMatch(g06.text, /Gute Besserung/)
|
||||
assert.equal(g06.icalEvent.method, 'CANCEL', 'die Kalenderabsage bleibt')
|
||||
assert.doesNotMatch(JSON.stringify(eintraege()), /Krankheit|Gute Besserung/)
|
||||
assert.equal(buchung(id).absageGrund, 'Krankheit', 'der Betrieb behaelt seinen Grund in der Buchung')
|
||||
assert.match(logText(), /G06: Empfänger-Kappe erreicht/)
|
||||
assert.doesNotMatch(logText(), /example\.com/)
|
||||
})
|
||||
|
||||
test('anGast: Freitext in G04/G06 des Betriebs geht bis zur dritten Mail je Adresse und Stunde mit, danach ohne; nach 61 Minuten wieder mit', async () => {
|
||||
const ctx = await speichere()
|
||||
const { buchung: b } = await gebucht(ctx)
|
||||
const g04 = (text, now) => sende('G04', ctx, b, { nachricht: text }, now)
|
||||
|
||||
assert.match((await g04('Eins', JETZT)).text, /Eins/)
|
||||
assert.match((await sende('G06', ctx, b, { von: 'betrieb', grund: 'Zwei', ohneIcs: true })).text, /Zwei/, 'G06 des Betriebs teilt sich die Zaehler mit G04')
|
||||
assert.match((await g04('Drei', JETZT)).text, /Drei/)
|
||||
const vier = await g04('Vier', JETZT)
|
||||
assert.doesNotMatch(vier.text, /Vier/)
|
||||
const fuenf = await sende('G06', ctx, b, { von: 'betrieb', grund: 'Fuenf', nachricht: 'Sechs', ohneIcs: true })
|
||||
assert.doesNotMatch(fuenf.text, /Fuenf|Sechs/, 'weder Grund noch Nachricht')
|
||||
assert.match(fuenf.text, /Salon Müller/)
|
||||
|
||||
assert.match((await g04('Sieben', plus(JETZT, 61 * MIN))).text, /Sieben/, 'nach 61 Minuten wieder')
|
||||
assert.doesNotMatch(logText(), /example\.com/)
|
||||
})
|
||||
|
||||
test('anGast: ohne Freitext zaehlt nichts - G04/G06 ohne Text, G06 des Gastes oder des Systems, G05 und die uebrigen Arten', async () => {
|
||||
const ctx = await speichere()
|
||||
const { buchung: b } = await gebucht(ctx)
|
||||
const vorher = sink.sent.length
|
||||
|
||||
await sende('G04', ctx, b, {})
|
||||
await sende('G04', ctx, b, { nachricht: ' ' })
|
||||
await sende('G06', ctx, b, { von: 'betrieb', ohneIcs: true })
|
||||
await sende('G06', ctx, b, { von: 'betrieb', grund: ' ', nachricht: '', ohneIcs: true })
|
||||
const gast = await sende('G06', ctx, b, { von: 'gast', grund: 'Gastgrund', nachricht: 'Gastnachricht', ohneIcs: true })
|
||||
await sende('G06', ctx, b, { von: 'system', ohneIcs: true })
|
||||
await sende('G05', ctx, b, {})
|
||||
await sende('G07', ctx, b, { altStart: b.startsAt, ohneIcs: true })
|
||||
assert.equal(sink.sent.length, vorher + 8)
|
||||
assert.match(gast.text, /Sie selbst/)
|
||||
assert.doesNotMatch(gast.text, /Gastgrund|Gastnachricht/, 'bei einer Absage des Gastes gibt es weder Grund noch Nachricht')
|
||||
assert.equal(log.warn.mock.callCount(), 0)
|
||||
|
||||
// die Kappe ist unberuehrt: alle drei Plaetze der Stunde sind frei
|
||||
assert.deepEqual([1, 2, 3, 4].map(() => darfEmpfaenger(GAST, JETZT)), [true, true, true, false])
|
||||
})
|
||||
|
||||
test('anGast: die Kappe gilt je Adresse (extra.an zaehlt klein geschrieben); eine andere Adresse ist nicht betroffen', async () => {
|
||||
const ctx = await speichere()
|
||||
const { buchung: b } = await gebucht(ctx)
|
||||
fuelleKappe(GAST, 3)
|
||||
fuelleKappe('weitere@example.com', 2)
|
||||
|
||||
const voll = await sende('G04', ctx, b, { nachricht: 'Nur fuer Max' })
|
||||
assert.doesNotMatch(voll.text, /Nur fuer Max/)
|
||||
const umgeleitet = await sende('G04', ctx, b, { nachricht: 'An eine andere Adresse', an: 'Weitere@Example.com' })
|
||||
assert.match(umgeleitet.text, /An eine andere Adresse/, 'die Adresse in extra.an zaehlt (klein geschrieben): dort war noch ein Platz')
|
||||
const nochmal = await sende('G04', ctx, b, { nachricht: 'Und noch eine', an: 'weitere@example.com' })
|
||||
assert.doesNotMatch(nochmal.text, /Und noch eine/, 'die Zaehler sind mit der Schreibweise der Adresse nicht zu umgehen')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- verschieben
|
||||
|
||||
test('verschieben: wie beim Gast (10:00 -> 10:30 ueberlappend), Sequenz + 1, neue Zellen, G07 mit Einladung, kein K21', async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
darfHaltung,
|
||||
darfWiderrufsversuch,
|
||||
darfG01,
|
||||
darfEmpfaenger,
|
||||
ipSchluessel,
|
||||
resetTermineLimitsForTests,
|
||||
} from '../server/services/termine/limits.js'
|
||||
@@ -159,13 +160,75 @@ test('darfWiderrufsversuch: 10 je IP und Stunde', () => {
|
||||
assert.equal(darfWiderrufsversuch('203.0.113.7', plus(STUNDE)), true)
|
||||
})
|
||||
|
||||
test('darfG01: 5 je Adresse und Funktion und Tag; die Zaehler der Arten stoeren sich nicht', () => {
|
||||
assert.deepEqual(Array.from({ length: 7 }, () => darfG01('f1', 'max@example.com', T0)), [true, true, true, true, true, false, false])
|
||||
assert.equal(darfG01('f2', 'max@example.com', T0), true, 'andere Funktion')
|
||||
test('darfG01: 3 je Adresse und Stunde ueber alle Funktionen; die Zaehler der Arten stoeren sich nicht', () => {
|
||||
assert.deepEqual(Array.from({ length: 7 }, () => darfG01('f1', 'max@example.com', T0)), [true, true, true, false, false, false, false])
|
||||
assert.equal(darfG01('f2', 'max@example.com', T0), false, 'die Kappe gilt ueber Betriebe (Funktionen) hinweg')
|
||||
assert.equal(darfG01('f1', 'anna@example.com', T0), true, 'andere Adresse')
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG - 1)), false)
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(TAG)), true, 'am naechsten Tag wieder')
|
||||
assert.equal(darfG08('f1', 'max@example.com', T0), true, 'G08 zaehlt getrennt von G01')
|
||||
assert.equal(darfG01('f1', 'max@example.com', plus(STUNDE - 1)), false, 'bis zur letzten Millisekunde der Stunde')
|
||||
assert.deepEqual(
|
||||
Array.from({ length: 4 }, () => darfG01('f2', 'max@example.com', plus(61 * MINUTE))),
|
||||
[true, true, true, false],
|
||||
'nach 61 Minuten wieder drei',
|
||||
)
|
||||
assert.deepEqual(
|
||||
Array.from({ length: 4 }, () => darfG08('f1', 'max@example.com', T0)),
|
||||
[true, true, true, false],
|
||||
'G08 zaehlt getrennt von G01 und hat ihre eigene Grenze von drei je Tag',
|
||||
)
|
||||
})
|
||||
|
||||
test('darfG01: je Adresse hoechstens 10 Empfaenger-Mails pro Tag (3 + 3 + 3 + 1 ueber vier Stunden), die 11. nicht', () => {
|
||||
// je Funktion gelten weiter hoechstens 5 G01 pro Tag; darum verteilen sich die zehn auf vier Funktionen
|
||||
const adresse = 'tag@example.com'
|
||||
const stunde = (n, funktion, anzahl) => Array.from({ length: anzahl }, () => darfG01(funktion, adresse, plus(n * 61 * MINUTE)))
|
||||
assert.deepEqual(stunde(0, 'f1', 3), [true, true, true])
|
||||
assert.deepEqual(stunde(1, 'f2', 3), [true, true, true])
|
||||
assert.deepEqual(stunde(2, 'f3', 3), [true, true, true])
|
||||
assert.deepEqual(stunde(3, 'f4', 2), [true, false], 'die zehnte geht, die elfte nicht (der Stundenzaehler hat dann erst eine)')
|
||||
assert.equal(darfG01('f5', adresse, plus(5 * 61 * MINUTE)), false, 'auch in einer neuen Stunde und mit unberuehrter Funktion')
|
||||
assert.equal(darfG01('f5', 'andere@example.com', plus(5 * 61 * MINUTE)), true, 'andere Adresse')
|
||||
assert.equal(darfG01('f5', adresse, plus(TAG)), true, 'am naechsten Tag wieder')
|
||||
})
|
||||
|
||||
test('darfG01: je Funktion hoechstens 5 pro Adresse und Tag (die Regel der Funktion bleibt neben der Kappe ueber Betriebe)', () => {
|
||||
const adresse = 'funktion@example.com'
|
||||
assert.deepEqual(Array.from({ length: 3 }, () => darfG01('f1', adresse, T0)), [true, true, true])
|
||||
assert.deepEqual(Array.from({ length: 3 }, () => darfG01('f1', adresse, plus(61 * MINUTE))), [true, true, false], 'die sechste an dieselbe Funktion nicht')
|
||||
assert.equal(darfG01('f2', adresse, plus(61 * MINUTE)), true, 'eine andere Funktion schon (noch ein Platz in der Stunde)')
|
||||
assert.equal(darfG01('f1', adresse, plus(TAG)), true, 'am naechsten Tag wieder')
|
||||
})
|
||||
|
||||
test('darfG01: alles oder nichts - eine abgelehnte Anfrage erhoeht keinen Zaehler, auch nicht den der Funktion', () => {
|
||||
const adresse = 'nichts@example.com'
|
||||
assert.deepEqual(Array.from({ length: 3 }, () => darfG01('f1', adresse, T0)), [true, true, true])
|
||||
// von der Stundenkappe abgelehnt: der Zaehler der Funktion (3 von 5) bleibt bei 3
|
||||
for (let i = 0; i < 6; i += 1) assert.equal(darfG01('f1', adresse, T0), false)
|
||||
assert.deepEqual(Array.from({ length: 3 }, () => darfG01('f1', adresse, plus(61 * MINUTE))), [true, true, false], 'danach noch genau 2 von 5 frei')
|
||||
// von der Funktionsregel abgelehnt: Stunden- und Tageszaehler (2 und 5) bleiben unveraendert
|
||||
for (let i = 0; i < 4; i += 1) assert.equal(darfG01('f1', adresse, plus(62 * MINUTE)), false)
|
||||
assert.deepEqual(Array.from({ length: 2 }, () => darfG01('f2', adresse, plus(62 * MINUTE))), [true, false], 'in der Stunde ist genau 1 von 3 frei')
|
||||
// bisher 6 am Tag gezaehlt (3 + 2 + 1): drei weitere und dann eine ergeben genau die zehn des Tages
|
||||
assert.deepEqual(Array.from({ length: 3 }, () => darfG01('f3', adresse, plus(123 * MINUTE))), [true, true, true])
|
||||
assert.deepEqual(Array.from({ length: 2 }, () => darfG01('f4', adresse, plus(184 * MINUTE))), [true, false], 'die zehnte war die letzte des Tages')
|
||||
})
|
||||
|
||||
test('darfEmpfaenger: teilt Stunden- und Tageszaehler mit darfG01, ohne die Regel der Funktion', () => {
|
||||
const adresse = 'frei@example.com'
|
||||
assert.deepEqual(
|
||||
[darfEmpfaenger(adresse, T0), darfEmpfaenger(adresse, T0), darfG01('f1', adresse, T0), darfG01('f1', adresse, T0)],
|
||||
[true, true, true, false],
|
||||
'zwei Freitext-Mails + zwei G01: die vierte Mail an dieselbe Adresse in der Stunde nicht',
|
||||
)
|
||||
assert.equal(darfEmpfaenger(adresse, T0), false)
|
||||
assert.equal(darfEmpfaenger('anna@example.com', T0), true, 'andere Adresse')
|
||||
assert.equal(darfEmpfaenger(adresse, plus(61 * MINUTE)), true, 'nach 61 Minuten wieder')
|
||||
|
||||
// die G01-Regel der Funktion (5 je Tag) zaehlt nur bei darfG01: fuenf G01 sind verbraucht, ein Freitext geht trotzdem
|
||||
const andere = 'regel@example.com'
|
||||
for (let i = 0; i < 3; i += 1) assert.equal(darfG01('f1', andere, plus(i * 61 * MINUTE)), true)
|
||||
for (let i = 0; i < 2; i += 1) assert.equal(darfG01('f1', andere, plus(3 * 61 * MINUTE)), true)
|
||||
assert.equal(darfG01('f1', andere, plus(3 * 61 * MINUTE)), false, 'sechste G01 an dieselbe Funktion')
|
||||
assert.equal(darfEmpfaenger(andere, plus(3 * 61 * MINUTE)), true, 'der Freitext kennt die Regel der Funktion nicht')
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Speicher: IP-Zaehler getrennt, Abgelaufene zuerst
|
||||
@@ -174,8 +237,8 @@ test('darfG01: 5 je Adresse und Funktion und Tag; die Zaehler der Arten stoeren
|
||||
const ipNr = (i) => `10.${(i >> 16) & 255}.${(i >> 8) & 255}.${i & 255}`
|
||||
|
||||
test('Speicher: 10 000 und mehr verschiedene IPs verdraengen keinen laufenden Zaehler je Adresse, Funktion oder siteKey', () => {
|
||||
// G01 einer Adresse ausgeschoepft, eine Haltungsgrenze angebrochen
|
||||
for (let i = 0; i < 5; i += 1) assert.equal(darfG01('f1', 'opfer@example.com', T0), true)
|
||||
// G01 einer Adresse ausgeschoepft (3 je Stunde), eine Haltungsgrenze angebrochen
|
||||
for (let i = 0; i < 3; i += 1) assert.equal(darfG01('f1', 'opfer@example.com', T0), true)
|
||||
assert.equal(darfG01('f1', 'opfer@example.com', T0), false)
|
||||
for (let i = 0; i < 25; i += 1) assert.equal(darfHaltung('abcdefghijklmnop', T0), true)
|
||||
assert.equal(darfG08('f1', 'opfer@example.com', T0), true)
|
||||
|
||||
Reference in new issue
Block a user