- GET /zeiten?ohne=<Buchung>: Leistung, Dauer und Person der Buchung wie beim Verschieben (verschiebeZeiten exportiert), unbekannte/fremde/Sperre 404 (R42) - Schreib-Begrenzung als gemeinsamer Baustein (writeLimiter.js) fuer News und Terminplaner statt kopierter Funktion (R43) - Unlesbares Einstellungs-JSON: Kontext traegt die echte Version, Speichern ueberschreibt den Schaden statt dauerhaft 409 - U+FEFF als Escape statt als rohes Zeichen - Tests: Mandantentrennung mit echtem zweitem Kunden, R42, Limiter, kaputtes JSON Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
84 lines
3.9 KiB
JavaScript
84 lines
3.9 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { createWriteLimiter } from '../server/services/writeLimiter.js'
|
|
|
|
// Schreib-Begrenzung je Schluessel im Speicher (News 30/h, Terminplaner 120/h): Fenster, getrennte Schluessel, Middleware.
|
|
const MINUTE = 60 * 1000
|
|
|
|
function uhr(start = 1_000_000) {
|
|
const stand = { jetzt: start }
|
|
return { stand, now: () => stand.jetzt }
|
|
}
|
|
|
|
test('writeLimiter: max Aufrufe sind erlaubt, der naechste ist darueber', () => {
|
|
const { now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 3, now })
|
|
assert.deepEqual([1, 2, 3].map(() => limiter.istUeberschritten('k')), [false, false, false])
|
|
assert.equal(limiter.istUeberschritten('k'), true)
|
|
assert.equal(limiter.istUeberschritten('k'), true, 'weitere Aufrufe bleiben darueber')
|
|
})
|
|
|
|
test('writeLimiter: Schluessel sind getrennt', () => {
|
|
const { now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 1, now })
|
|
assert.equal(limiter.istUeberschritten('a'), false)
|
|
assert.equal(limiter.istUeberschritten('a'), true)
|
|
assert.equal(limiter.istUeberschritten('b'), false, 'b hat sein eigenes Limit')
|
|
})
|
|
|
|
test('writeLimiter: das Fenster laeuft ab (Standard eine Stunde), ein Aufruf zaehlt genau bis 60 Minuten nach ihm', () => {
|
|
const { stand, now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 2, now })
|
|
assert.equal(limiter.istUeberschritten('k'), false) // A bei 0
|
|
stand.jetzt += 60 * MINUTE - 1
|
|
assert.equal(limiter.istUeberschritten('k'), false) // B bei 59:59,999: A zaehlt noch (2 <= 2)
|
|
stand.jetzt += 1 // 60:00: A ist aus dem Fenster
|
|
assert.equal(limiter.istUeberschritten('k'), false) // C: B + C = 2
|
|
assert.equal(limiter.istUeberschritten('k'), true) // D: B + C + D = 3 > 2
|
|
})
|
|
|
|
test('writeLimiter: auch abgelehnte Aufrufe zaehlen im Fenster mit (wie bisher bei den News)', () => {
|
|
const { stand, now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 1, now })
|
|
assert.equal(limiter.istUeberschritten('k'), false) // X bei 0
|
|
stand.jetzt += MINUTE
|
|
assert.equal(limiter.istUeberschritten('k'), true) // Y bei 1 min, abgelehnt
|
|
stand.jetzt += 59 * MINUTE // 60 min: X ist raus, Y (59 min alt) zaehlt noch
|
|
assert.equal(limiter.istUeberschritten('k'), true)
|
|
})
|
|
|
|
test('writeLimiter: eigenes Fenster, abgelaufene Schluessel werden vergessen, reset leert alles', () => {
|
|
const { stand, now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 1, fensterMs: 10 * MINUTE, now })
|
|
assert.equal(limiter.istUeberschritten('a'), false)
|
|
assert.equal(limiter.istUeberschritten('a'), true)
|
|
stand.jetzt += 10 * MINUTE
|
|
assert.equal(limiter.istUeberschritten('a'), false, 'nach dem Fenster frei')
|
|
assert.equal(limiter.istUeberschritten('b'), false)
|
|
limiter.reset()
|
|
assert.equal(limiter.istUeberschritten('a'), false)
|
|
assert.equal(limiter.istUeberschritten('b'), false)
|
|
})
|
|
|
|
test('writeLimiter: middleware antwortet ab dem Ueberschreiten mit 429 und dem Text, sonst weiter', () => {
|
|
const { now } = uhr()
|
|
const limiter = createWriteLimiter({ max: 2, now })
|
|
const mw = limiter.middleware((req) => req.kunde, 'Zu viele Änderungen.')
|
|
const aufruf = (kunde) => {
|
|
const antwort = { status(code) { this.code = code; return this }, json(body) { this.body = body; return this } }
|
|
let weiter = 0
|
|
mw({ kunde }, antwort, () => { weiter += 1 })
|
|
return { weiter, code: antwort.code, body: antwort.body }
|
|
}
|
|
assert.deepEqual(aufruf('c1'), { weiter: 1, code: undefined, body: undefined })
|
|
assert.equal(aufruf('c1').weiter, 1)
|
|
assert.deepEqual(aufruf('c1'), { weiter: 0, code: 429, body: { error: 'Zu viele Änderungen.' } })
|
|
assert.equal(aufruf('c2').weiter, 1, 'anderer Kunde')
|
|
})
|
|
|
|
test('writeLimiter: ungueltige Angaben werfen', () => {
|
|
for (const max of [0, -1, 1.5, '3', undefined]) assert.throws(() => createWriteLimiter({ max }), TypeError, String(max))
|
|
assert.throws(() => createWriteLimiter({ max: 1, fensterMs: 0 }), TypeError)
|
|
assert.throws(() => createWriteLimiter({ max: 1, now: 5 }), TypeError)
|
|
})
|