285 lines
17 KiB
JavaScript
285 lines
17 KiB
JavaScript
import { test, beforeEach } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {
|
|
zaehle,
|
|
zaehleAlle,
|
|
darfG08,
|
|
darfK21NichtZugeordnet,
|
|
darfAnfrage,
|
|
darfAnfrageSite,
|
|
darfBuchungsversuch,
|
|
darfHaltung,
|
|
darfWiderrufsversuch,
|
|
darfG01,
|
|
darfEmpfaenger,
|
|
ipSchluessel,
|
|
resetTermineLimitsForTests,
|
|
} from '../server/services/termine/limits.js'
|
|
|
|
// Grenzen im Speicher: festes Fenster ab dem ersten Ereignis, alle-oder-keine bei mehreren Regeln, Obergrenze der Schluessel.
|
|
const T0 = new Date('2026-10-05T08:00:00.000Z')
|
|
const STUNDE = 60 * 60 * 1000
|
|
const TAG = 24 * STUNDE
|
|
const MINUTE = 60 * 1000
|
|
const plus = (ms) => new Date(T0.getTime() + ms)
|
|
|
|
beforeEach(() => resetTermineLimitsForTests())
|
|
|
|
test('zaehle: erlaubt bis max, danach false; das Fenster beginnt mit dem ersten Ereignis und endet nach fensterMs', () => {
|
|
assert.deepEqual([1, 2, 3].map(() => zaehle('a', 3, STUNDE, T0)), [true, true, true])
|
|
assert.equal(zaehle('a', 3, STUNDE, plus(10 * 60000)), false)
|
|
assert.equal(zaehle('a', 3, STUNDE, plus(STUNDE - 1)), false, 'bis zur letzten Millisekunde des Fensters')
|
|
assert.equal(zaehle('b', 3, STUNDE, T0), true, 'andere Schluessel zaehlen getrennt')
|
|
// die abgelehnten Anfragen haben nichts gezaehlt: das neue Fenster (ab plus(STUNDE)) erlaubt wieder drei
|
|
assert.deepEqual([1, 2, 3, 4].map((i) => zaehle('a', 3, STUNDE, plus(STUNDE + i))), [true, true, true, false])
|
|
})
|
|
|
|
test('zaehle: max 0 erlaubt nie, ungueltige Angaben werfen TypeError', () => {
|
|
assert.equal(zaehle('a', 0, STUNDE, T0), false)
|
|
assert.throws(() => zaehle('a', 1.5, STUNDE, T0), TypeError)
|
|
assert.throws(() => zaehle('a', -1, STUNDE, T0), TypeError)
|
|
assert.throws(() => zaehle('a', 1, 0, T0), TypeError)
|
|
assert.throws(() => zaehle('', 1, STUNDE, T0), TypeError)
|
|
assert.throws(() => zaehle('a', 1, STUNDE, undefined), TypeError)
|
|
assert.throws(() => zaehle('a', 1, STUNDE, Date.now()), TypeError)
|
|
assert.throws(() => zaehleAlle([], T0), TypeError)
|
|
})
|
|
|
|
test('zaehleAlle: alle oder keine - scheitert eine Regel, zaehlt keine', () => {
|
|
const regeln = [
|
|
{ schluessel: 'adresse', max: 3, fensterMs: TAG },
|
|
{ schluessel: 'funktion', max: 1, fensterMs: STUNDE },
|
|
]
|
|
assert.equal(zaehleAlle(regeln, T0), true)
|
|
assert.equal(zaehleAlle(regeln, plus(1000)), false, 'die Funktionsgrenze ist erreicht')
|
|
// der Adress-Zaehler wurde dabei nicht verbraucht: nach der Stunde sind noch zwei Ereignisse an diese Adresse frei
|
|
const nurAdresse = [{ schluessel: 'adresse', max: 3, fensterMs: TAG }]
|
|
assert.equal(zaehleAlle(nurAdresse, plus(2 * STUNDE)), true)
|
|
assert.equal(zaehleAlle(nurAdresse, plus(2 * STUNDE)), true)
|
|
assert.equal(zaehleAlle(nurAdresse, plus(2 * STUNDE)), false)
|
|
})
|
|
|
|
test('darfG08: 3 je Adresse und Tag (auch ueber Funktionen), 20 je Funktion und Stunde', () => {
|
|
assert.deepEqual([1, 2, 3, 4].map(() => darfG08('f1', 'max@example.com', T0)), [true, true, true, false])
|
|
assert.equal(darfG08('f2', 'max@example.com', plus(TAG - 1)), false, 'die Adresse zaehlt fuer alle Funktionen zusammen')
|
|
assert.equal(darfG08('f1', 'max@example.com', plus(TAG)), true, 'am naechsten Tag wieder')
|
|
|
|
resetTermineLimitsForTests()
|
|
const erlaubt = Array.from({ length: 22 }, (_, i) => darfG08('f1', `gast${i}@example.com`, T0))
|
|
assert.equal(erlaubt.filter(Boolean).length, 20)
|
|
assert.equal(darfG08('f2', 'neu@example.com', T0), true, 'andere Funktion hat ihr eigenes Kontingent')
|
|
assert.equal(darfG08('f1', 'spaet@example.com', plus(STUNDE)), true, 'nach einer Stunde wieder')
|
|
})
|
|
|
|
test('darfK21NichtZugeordnet: 30 je Funktion und Stunde', () => {
|
|
assert.equal(Array.from({ length: 32 }, () => darfK21NichtZugeordnet('f1', T0)).filter(Boolean).length, 30)
|
|
assert.equal(darfK21NichtZugeordnet('f2', T0), true)
|
|
assert.equal(darfK21NichtZugeordnet('f1', plus(STUNDE)), true)
|
|
})
|
|
|
|
test('Obergrenze: ueber 10 000 Schluessel faellt der aelteste Zaehler weg, die neueren bleiben', () => {
|
|
assert.equal(zaehle('aeltester', 1, TAG, T0), true)
|
|
assert.equal(zaehle('aeltester', 1, TAG, T0), false)
|
|
for (let i = 0; i < 9998; i += 1) zaehle(`k${i}`, 1, TAG, plus(1000))
|
|
assert.equal(zaehle('aeltester', 1, TAG, T0), false, 'bei 9 999 Schluesseln noch da')
|
|
assert.equal(zaehle('k9997', 1, TAG, plus(2000)), false)
|
|
zaehle('neu1', 1, TAG, plus(3000)) // 10 000
|
|
zaehle('neu2', 1, TAG, plus(3000)) // 10 001: der aelteste faellt weg
|
|
assert.equal(zaehle('aeltester', 1, TAG, plus(4000)), true, 'der aelteste Zaehler wurde verdraengt')
|
|
assert.equal(zaehle('neu2', 1, TAG, plus(4000)), false, 'der neueste bleibt')
|
|
assert.equal(zaehle('k9997', 1, TAG, plus(4000)), false, 'die juengeren bleiben')
|
|
})
|
|
|
|
test('resetTermineLimitsForTests leert alles', () => {
|
|
zaehle('a', 1, TAG, T0)
|
|
assert.equal(zaehle('a', 1, TAG, T0), false)
|
|
resetTermineLimitsForTests()
|
|
assert.equal(zaehle('a', 1, TAG, T0), true)
|
|
})
|
|
|
|
// ---------------------------------------------------------------- Aufgabe 16: IP, siteKey, G01
|
|
|
|
test('ipSchluessel: IPv4 bleibt, IPv4-in-IPv6 wird IPv4, IPv6 zaehlt je /64 (auch in anderer Schreibweise)', () => {
|
|
assert.equal(ipSchluessel('203.0.113.7'), '203.0.113.7')
|
|
assert.equal(ipSchluessel('::ffff:203.0.113.7'), '203.0.113.7')
|
|
assert.equal(ipSchluessel('::FFFF:203.0.113.7'), '203.0.113.7')
|
|
const a = ipSchluessel('2001:db8:1:2::1')
|
|
assert.equal(ipSchluessel('2001:0db8:0001:0002:ffff:ffff:ffff:ffff'), a, 'dieselbe /64, ausgeschrieben')
|
|
assert.equal(ipSchluessel('2001:DB8:1:2:abcd::9'), a, 'dieselbe /64, Grossbuchstaben und Kurzform')
|
|
assert.equal(ipSchluessel('2001:db8:1:2:0:0:0:0'), a)
|
|
assert.notEqual(ipSchluessel('2001:db8:1:3::1'), a, 'andere /64')
|
|
assert.notEqual(ipSchluessel('2001:db8:2:2::1'), a)
|
|
assert.equal(ipSchluessel('fe80::1%eth0'), ipSchluessel('fe80::2'), 'Zonenkennung faellt weg')
|
|
assert.equal(ipSchluessel('::1'), ipSchluessel('0:0:0:0:ffff:ffff:ffff:ffff'), 'Loopback ist die /64 ::')
|
|
assert.notEqual(ipSchluessel('203.0.113.7'), ipSchluessel('203.0.113.8'))
|
|
assert.notEqual(ipSchluessel('203.0.113.7'), a, 'IPv4 und IPv6 stossen nie zusammen')
|
|
})
|
|
|
|
test('ipSchluessel: fehlende oder unlesbare Adresse ergibt einen festen Ersatzschluessel (nie einen Fehler)', () => {
|
|
for (const wert of [undefined, null, '', ' ', 'kaputt', '1.2.3', '999.1.1.1', 42, {}]) {
|
|
assert.equal(ipSchluessel(wert), 'unbekannt', JSON.stringify(wert))
|
|
}
|
|
})
|
|
|
|
test('darfAnfrage: 120 je IP und Minute (IPv6 je /64); darfAnfrageSite: 600 je siteKey und Minute', () => {
|
|
assert.equal(Array.from({ length: 125 }, () => darfAnfrage('203.0.113.7', T0)).filter(Boolean).length, 120)
|
|
assert.equal(darfAnfrage('203.0.113.8', T0), true, 'andere IP')
|
|
assert.equal(darfAnfrage('203.0.113.7', plus(MINUTE)), true, 'nach einer Minute wieder')
|
|
|
|
resetTermineLimitsForTests()
|
|
const erlaubt = Array.from({ length: 125 }, (_, i) => darfAnfrage(`2001:db8:1:2::${i + 1}`, T0))
|
|
assert.equal(erlaubt.filter(Boolean).length, 120, 'alle Adressen der /64 teilen sich 120')
|
|
assert.equal(darfAnfrage('2001:db8:1:3::1', T0), true)
|
|
|
|
resetTermineLimitsForTests()
|
|
assert.equal(Array.from({ length: 605 }, () => darfAnfrageSite('abcdefghijklmnop', T0)).filter(Boolean).length, 600)
|
|
assert.equal(darfAnfrageSite('bcdefghijklmnopq', T0), true)
|
|
assert.equal(darfAnfrageSite('abcdefghijklmnop', plus(MINUTE)), true)
|
|
})
|
|
|
|
test('darfBuchungsversuch: 10 je IP und 10 Minuten (IPv6 je /64); darfHaltung: 25 je siteKey und Stunde', () => {
|
|
assert.equal(Array.from({ length: 12 }, () => darfBuchungsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
|
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE - 1)), false)
|
|
assert.equal(darfBuchungsversuch('203.0.113.7', plus(10 * MINUTE)), true)
|
|
assert.equal(darfBuchungsversuch('2001:db8::1', T0) && darfBuchungsversuch('2001:db8:0:0:1::1', T0), true, 'getrennt von der IPv4')
|
|
|
|
resetTermineLimitsForTests()
|
|
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9::${i + 1}`, T0), true)
|
|
for (let i = 0; i < 5; i += 1) assert.equal(darfBuchungsversuch(`2001:db8:9:9:1::${i + 1}`, T0), true)
|
|
assert.equal(darfBuchungsversuch('2001:db8:9:9:ffff::1', T0), false, 'elf Versuche aus einer /64')
|
|
|
|
resetTermineLimitsForTests()
|
|
assert.equal(Array.from({ length: 65 }, () => darfHaltung('abcdefghijklmnop', T0)).filter(Boolean).length, 25)
|
|
assert.equal(darfHaltung('bcdefghijklmnopq', T0), true)
|
|
assert.equal(darfHaltung('abcdefghijklmnop', plus(STUNDE)), true)
|
|
})
|
|
|
|
test('darfWiderrufsversuch: 10 je IP und Stunde', () => {
|
|
assert.equal(Array.from({ length: 12 }, () => darfWiderrufsversuch('203.0.113.7', T0)).filter(Boolean).length, 10)
|
|
assert.equal(darfWiderrufsversuch('203.0.113.8', T0), true)
|
|
assert.equal(darfWiderrufsversuch('203.0.113.7', plus(STUNDE)), true)
|
|
})
|
|
|
|
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(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
|
|
|
|
/** Eindeutige IPv4-Adresse Nr. i (10.a.b.c). */
|
|
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 (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)
|
|
|
|
// ueber 10 000 verschiedene Quelladressen: alle drei IP-Arten
|
|
for (let i = 0; i < 10500; i += 1) darfAnfrage(ipNr(i), T0)
|
|
for (let i = 0; i < 10500; i += 1) darfBuchungsversuch(ipNr(i), plus(1000))
|
|
for (let i = 0; i < 10500; i += 1) darfWiderrufsversuch(ipNr(i), plus(2000))
|
|
|
|
assert.equal(darfG01('f1', 'opfer@example.com', plus(3000)), false, 'die G01-Grenze der Adresse gilt weiter')
|
|
assert.equal(darfHaltung('abcdefghijklmnop', plus(3000)), false, 'die Haltungsgrenze gilt weiter')
|
|
assert.equal(darfG08('f1', 'opfer@example.com', plus(3000)), true, 'G08 zaehlt weiter ab 1 (2 von 3 frei)')
|
|
assert.equal(darfG08('f1', 'opfer@example.com', plus(3000)), true)
|
|
assert.equal(darfG08('f1', 'opfer@example.com', plus(3000)), false, 'und die dritte war die letzte')
|
|
})
|
|
|
|
test('Speicher: eine volle Map raeumt zuerst Abgelaufene ab - ein laufender Zaehler bleibt, auch der aelteste', () => {
|
|
// der aelteste Zaehler laeuft eine Stunde; danach fuellen Minuten-Zaehler die IP-Map
|
|
for (let i = 0; i < 10; i += 1) assert.equal(darfWiderrufsversuch('203.0.113.7', T0), true)
|
|
assert.equal(darfWiderrufsversuch('203.0.113.7', T0), false)
|
|
for (let i = 0; i < 9999; i += 1) darfAnfrage(ipNr(i), T0) // Map voll (10 000 Schluessel)
|
|
// zwei Minuten spaeter sind alle Minuten-Zaehler abgelaufen: neue Schluessel schaffen Platz, ohne den laufenden zu verdraengen
|
|
for (let i = 0; i < 300; i += 1) darfAnfrage(ipNr(20000 + i), plus(2 * MINUTE))
|
|
assert.equal(darfWiderrufsversuch('203.0.113.7', plus(2 * MINUTE)), false, 'der Stunden-Zaehler ueberlebt')
|
|
assert.equal(darfAnfrage(ipNr(5), plus(2 * MINUTE)), true, 'abgelaufene Zaehler zaehlen neu')
|
|
|
|
// dasselbe in der Map fuer alle anderen Zaehler
|
|
resetTermineLimitsForTests()
|
|
assert.equal(zaehle('alt', 1, TAG, T0), true)
|
|
for (let i = 0; i < 9999; i += 1) zaehle(`k${i}`, 1, MINUTE, T0)
|
|
for (let i = 0; i < 300; i += 1) zaehle(`neu${i}`, 1, MINUTE, plus(2 * MINUTE))
|
|
assert.equal(zaehle('alt', 1, TAG, plus(2 * MINUTE)), false, 'der Tages-Zaehler ueberlebt')
|
|
})
|
|
|
|
test('Speicher: sind alle Zaehler noch am Laufen, faellt wie bisher der aelteste weg (Obergrenze gilt je Map)', () => {
|
|
assert.equal(zaehle('aeltester', 1, TAG, T0), true)
|
|
for (let i = 0; i < 9999; i += 1) zaehle(`k${i}`, 1, TAG, plus(1000))
|
|
for (let i = 0; i < 10000; i += 1) darfAnfrage(ipNr(i), plus(1000)) // die IP-Map ist eine andere
|
|
assert.equal(zaehle('aeltester', 1, TAG, plus(2000)), false, 'durch die IP-Zaehler nicht verdraengt')
|
|
zaehle('neu', 1, TAG, plus(3000)) // 10 001. Schluessel der Map: der aelteste faellt weg
|
|
assert.equal(zaehle('aeltester', 1, TAG, plus(4000)), true)
|
|
assert.equal(zaehle('neu', 1, TAG, plus(4000)), false)
|
|
})
|