feat(nextcloud-files): Modul Dateien mit Nextcloud-Adresse, Verbindungsprüfung und gesicherter Verbindungsschicht
- Migration 20261008180000: NextcloudFilesConfig (Organisation) und NextcloudFilesAccount (Mandant UND Benutzer per Zeilenschutz, nur verschlüsseltes App-Passwort) - Transportschicht ncRequest: feste Pfadanfänge, Segmentcodierung, keine Weiterleitungen, keine Cookies, Zeitgrenzen; Aufrufsperre pausiert den Ursprung nach jedem 429 und sperrt Zugangsschlüssel nach dem ersten 401 - Einstellungen: Adresse speichern/prüfen, Adresswechsel läuft alle Konten ab (mit Bestätigung) - Controller mit Verwalten nur auf Einstellungen, Seed, Modul, Registrierung in Web (Ladefunktion, Symbol folder, Navigation, Layout) und Reiter Einstellungen - RLS-Inventar fortgeschrieben, Testaufbau (tessera-nc-test) und E2E-Skripte Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user