import { describe, expect, it } from 'vitest'; import { DEAD_KEY_TTL_MS, DEFAULT_PAUSE_SECONDS, MAX_DEAD_KEYS, MAX_PAUSE_SECONDS, NextcloudCallGate, } from './nextcloud-call-gate'; function makeGate() { const gate = new NextcloudCallGate(); const clock = { t: 1_000_000 }; gate.now = () => clock.t; return { gate, clock }; } describe('NextcloudCallGate — Sperre je Ursprung', () => { it('nach pause(): 900 Sekunden, nur fuer diesen Ursprung', () => { const { gate } = makeGate(); expect(gate.isPaused('https://cloud.example')).toEqual({ paused: false, retryAfterSeconds: 0 }); gate.pause('https://cloud.example'); expect(gate.isPaused('https://cloud.example')).toEqual({ paused: true, retryAfterSeconds: DEFAULT_PAUSE_SECONDS, }); expect(DEFAULT_PAUSE_SECONDS).toBe(900); expect(gate.isPaused('https://other.example').paused).toBe(false); }); it('Retry-After 120 -> 120 Sekunden; 99999 -> auf 3600 gedeckelt; Unsinn -> 900', () => { const { gate } = makeGate(); gate.pause('https://a.example', '120'); expect(gate.isPaused('https://a.example').retryAfterSeconds).toBe(120); gate.pause('https://b.example', '99999'); expect(gate.isPaused('https://b.example').retryAfterSeconds).toBe(3600); expect(MAX_PAUSE_SECONDS).toBe(3600); gate.pause('https://c.example', 'Wed, 21 Oct 2026 07:28:00 GMT'); expect(gate.isPaused('https://c.example').retryAfterSeconds).toBe(900); gate.pause('https://d.example', '0'); expect(gate.isPaused('https://d.example').retryAfterSeconds).toBe(900); }); it('nach Ablauf geht es wieder, die Restzeit sinkt mit der Uhr', () => { const { gate, clock } = makeGate(); gate.pause('https://cloud.example', '120'); clock.t += 60_000; expect(gate.isPaused('https://cloud.example')).toEqual({ paused: true, retryAfterSeconds: 60 }); clock.t += 60_000; expect(gate.isPaused('https://cloud.example').paused).toBe(false); }); it('eine kuerzere zweite Sperre verkuerzt eine laengere nicht', () => { const { gate } = makeGate(); gate.pause('https://cloud.example', '600'); gate.pause('https://cloud.example', '60'); expect(gate.isPaused('https://cloud.example').retryAfterSeconds).toBe(600); }); }); describe('NextcloudCallGate — Sperre je Zugangsschluessel', () => { it('markDead(): Schluessel tot, andere Schluessel nicht', () => { const { gate } = makeGate(); expect(gate.isDead('k1')).toBe(false); gate.markDead('k1'); expect(gate.isDead('k1')).toBe(true); expect(gate.isDead('k2')).toBe(false); }); it('markDead() bricht das Signal des Schluessels ab, nicht das eines anderen', () => { const { gate } = makeGate(); const s1 = gate.signalFor('k1'); const s2 = gate.signalFor('k2'); expect(s1.aborted).toBe(false); gate.markDead('k1'); expect(s1.aborted).toBe(true); expect(s2.aborted).toBe(false); expect(gate.signalFor('k1').aborted).toBe(true); }); it('tote Schluessel laufen nach 24 Stunden ab', () => { const { gate, clock } = makeGate(); gate.markDead('k1'); clock.t += DEAD_KEY_TTL_MS - 1; expect(gate.isDead('k1')).toBe(true); clock.t += 2; expect(gate.isDead('k1')).toBe(false); }); it('haelt nie mehr als 10000 tote Schluessel, die aeltesten fliegen zuerst raus', () => { const { gate } = makeGate(); for (let i = 0; i < MAX_DEAD_KEYS + 5; i++) gate.markDead(`key-${i}`); expect(gate.isDead('key-0')).toBe(false); expect(gate.isDead('key-4')).toBe(false); expect(gate.isDead('key-5')).toBe(true); expect(gate.isDead(`key-${MAX_DEAD_KEYS + 4}`)).toBe(true); // intern gezaehlt: genau MAX_DEAD_KEYS Schluessel sind noch tot let dead = 0; for (let i = 0; i < MAX_DEAD_KEYS + 5; i++) if (gate.isDead(`key-${i}`)) dead++; expect(dead).toBe(MAX_DEAD_KEYS); }); });