import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { SchulcloudApiError } from '../src/core/client.ts'; import { SessionKeepalive } from '../src/core/keepalive.ts'; /** A stand-in for SchulcloudClient that records calls and replays scripted outcomes. */ function fakeClient(outcomes: (Error | 'ok')[]) { const calls: number[] = []; return { calls, client: { extendSession: async () => { const outcome = outcomes[calls.length] ?? 'ok'; calls.push(Date.now()); if (outcome !== 'ok') throw outcome; return { expiresInSeconds: 7200 }; }, } as never, }; } const settle = () => new Promise((resolve) => setTimeout(resolve, 30)); describe('SessionKeepalive', () => { it('pings immediately on start, so a bad token is noticed at boot', async () => { const { client, calls } = fakeClient(['ok']); const keepalive = new SessionKeepalive(client, 60_000, 1000, () => {}); keepalive.start(); await settle(); assert.equal(calls.length, 1); keepalive.stop(); }); it('keeps pinging on the interval', async () => { const { client, calls } = fakeClient([]); const keepalive = new SessionKeepalive(client, 15, 15, () => {}); keepalive.start(); await new Promise((resolve) => setTimeout(resolve, 120)); keepalive.stop(); assert.ok(calls.length >= 3, `expected repeated pings, got ${calls.length}`); }); it('stops permanently on 401 — a dead session cannot be revived by retrying', async () => { const unauthorized = new SchulcloudApiError(401, '/api/v3/authentication/refresh-session', ''); const { client, calls } = fakeClient([unauthorized]); const messages: string[] = []; const keepalive = new SessionKeepalive(client, 15, 15, (message) => messages.push(message)); keepalive.start(); await new Promise((resolve) => setTimeout(resolve, 120)); assert.equal(calls.length, 1, 'must not keep hammering a dead token'); assert.equal(messages.length, 1); assert.match(messages[0]!, /fresh jwt cookie/); keepalive.stop(); }); it('retries a transient failure instead of giving up', async () => { const { client, calls } = fakeClient([new Error('ECONNRESET')]); const messages: string[] = []; const keepalive = new SessionKeepalive(client, 10_000, 15, (message) => messages.push(message)); keepalive.start(); await new Promise((resolve) => setTimeout(resolve, 120)); keepalive.stop(); assert.ok(calls.length >= 2, `expected a retry, got ${calls.length} call(s)`); assert.match(messages[0]!, /retrying in/); }); it('stop() prevents any further pings', async () => { const { client, calls } = fakeClient([]); const keepalive = new SessionKeepalive(client, 15, 15, () => {}); keepalive.start(); await settle(); keepalive.stop(); const seen = calls.length; await new Promise((resolve) => setTimeout(resolve, 100)); assert.equal(calls.length, seen, 'no pings after stop()'); }); }); describe('SessionKeepalive logging', () => { it('reports the remaining session budget on a successful extension', async () => { const messages: string[] = []; const client = { extendSession: async () => ({ expiresInSeconds: 7200 }) } as never; const keepalive = new SessionKeepalive(client, 60_000, 1000, (m) => messages.push(m)); keepalive.start(); await new Promise((resolve) => setTimeout(resolve, 30)); keepalive.stop(); assert.equal(messages.length, 1); assert.match(messages[0]!, /session extended, 7200s \(120 min\)/); }); });