diff --git a/tests/unit/workersApi.test.mjs b/tests/unit/workersApi.test.mjs index 6f03754..ffb787e 100644 --- a/tests/unit/workersApi.test.mjs +++ b/tests/unit/workersApi.test.mjs @@ -207,6 +207,112 @@ test('Zeitgrenze: Standardwerte 20 s und 2 min, für langsame Pult-Aktionen 65 s assert.ok(Object.isFrozen(UNCERTAIN_STATUS)) }) +// P4-T32 (D14), Nachrunde R7: Antworten, Freigeben und Bauen laufen im Dienst länger (SMTP, Warteschlange); bricht der Client nach 20 s ab, sähe der +// Mensch einen Fehler, obwohl die Mail hinausgeht. Nur diese drei Aktionen bekommen die lange Frist (65 s), alles andere bleibt bei der kurzen (20 s). +// Mit den Uhren von node:test statt echter Wartezeit: kein Warten, keine Abhängigkeit von der Last des Rechners, und die Grenzen sind auf die Millisekunde +// genau prüfbar (eine Millisekunde vor der Frist läuft der Abruf noch, mit ihr endet er). setImmediate bleibt echt und lässt die Promise-Ketten durchlaufen. +const flush = () => new Promise((resolve) => setImmediate(resolve)) +const bodyFor = (action) => (action === 'reply' || action === 'paste' ? { text: 'x' } : undefined) + +// Dienst, der erst nach ms antwortet (Uhr von node:test); merkt sich die Signale der Abrufe +function slowService(ms, signals = []) { + return (url, init) => { + signals.push(init.signal) + return new Promise((resolve) => setTimeout(() => resolve({ ok: true, status: 200, json: async () => ({ item: { $id: 'p1' } }) }), ms)) + } +} + +// Ergebnis eines Abrufs, der noch läuft, ohne auf ihn zu warten: 'offen', 'ok' oder der Fehler +function watch(promise) { + const w = { state: 'offen', error: null } + promise.then(() => { w.state = 'ok' }, (e) => { w.state = 'fehler'; w.error = e }) + return w +} + +// Standardfristen (20 s / 65 s) und eingespeiste Fristen (timeoutMs / actionTimeoutMs) müssen sich gleich verhalten +for (const [label, options] of [ + ['Standardfristen', {}], + ['eingespeiste Fristen', { timeoutMs: 300, actionTimeoutMs: 4000 }], +]) { + test(`Zeitgrenze (D14): reply, approve-preview und start-build warten länger als die übrigen Pult-Aktionen (${label})`, async (t) => { + t.mock.timers.enable({ apis: ['setTimeout'] }) + const short = options.timeoutMs ?? REQUEST_TIMEOUT_MS + const long = options.actionTimeoutMs ?? ACTION_TIMEOUT_MS + assert.ok(short < long) + const between = Math.floor((short + long) / 2) // die Antwort kommt nach der kurzen, aber vor der langen Frist + const run = async (action, { service = slowService(between) }) => { + const api = createWorkersApi({ getJwt, fetchImpl: service, ...options }) + const w = watch(action === 'read' ? api.pult() : api.pultAction('p1', action, bodyFor(action))) + await flush() // JWT geholt, Anfrage gesendet, Frist und Antwort stehen auf der Uhr + return { w, api, tick: async (ms) => { t.mock.timers.tick(ms); await flush() } } + } + + // die drei langsamen Aktionen: die Antwort nach der kurzen Frist kommt noch an + for (const action of ['reply', 'approve-preview', 'start-build']) { + const { w, tick } = await run(action, {}) + await tick(short) // die kurze Frist ist erreicht: der Abruf läuft weiter + assert.equal(w.state, 'offen', `${action}: nach der kurzen Frist noch offen`) + await tick(between - short) // die Antwort trifft ein + assert.equal(w.state, 'ok', `${action}: Antwort nach der kurzen Frist, vor der langen`) + } + + // alle übrigen Aktionen und das Lesen behalten die kurze Frist: eine Millisekunde davor offen, mit ihr „nicht erreichbar“ (status 0), Signal abgebrochen + for (const action of ['claim', 'sent', 'discard', 'done', 'paste', 'read']) { + const signals = [] + const { w, tick } = await run(action, { service: slowService(between, signals) }) + await tick(short - 1) + assert.equal(w.state, 'offen', `${action}: eine Millisekunde vor der kurzen Frist`) + assert.equal(signals[0].aborted, false, action) + await tick(1) + assert.equal(w.state, 'fehler', `${action}: mit der kurzen Frist`) + assert.equal(w.error.message, DOWN, action) + assert.equal(w.error.status, 0, action) + assert.equal(signals[0].aborted, true, `${action}: das Signal ist abgebrochen`) + } + + // auch die lange Frist endet: ein hängender Abruf meldet nach ihr die Dienst-Meldung (status 0) und bricht ab, keine Millisekunde früher + for (const action of ['reply', 'approve-preview', 'start-build']) { + const signals = [] + const hang = (url, init) => { signals.push(init.signal); return new Promise(() => {}) } + const { w, tick } = await run(action, { service: hang }) + await tick(long - 1) + assert.equal(w.state, 'offen', `${action}: eine Millisekunde vor der langen Frist`) + assert.equal(signals[0].aborted, false, action) + await tick(1) + assert.equal(w.state, 'fehler', `${action}: mit der langen Frist`) + assert.deepEqual([w.error.message, w.error.status], [DOWN, 0], action) + assert.equal(signals[0].aborted, true, `${action}: das Signal ist abgebrochen`) + } + }) +} + +test('Zeitgrenze (D14): auch der Wiederholungsversuch nach einer 401 behält die lange Frist bei den langsamen Aktionen, die kurze bei den übrigen', async (t) => { + t.mock.timers.enable({ apis: ['setTimeout'] }) + const MIDDLE = Math.floor((REQUEST_TIMEOUT_MS + ACTION_TIMEOUT_MS) / 2) + const slowAfter401 = () => { + let n = 0 + return (url, init) => { + n += 1 + if (n === 1) return Promise.resolve({ ok: false, status: 401, json: async () => ({}) }) + return slowService(MIDDLE)(url, init) // zweiter Versuch: Antwort nach der kurzen (20 s), vor der langen Frist (65 s) + } + } + const tick = async (ms) => { t.mock.timers.tick(ms); await flush() } + for (const action of ['reply', 'approve-preview', 'start-build']) { + const w = watch(createWorkersApi({ getJwt, fetchImpl: slowAfter401() }).pultAction('p1', action, bodyFor(action))) + await flush() + await tick(MIDDLE) + assert.equal(w.state, 'ok', action) + } + for (const action of ['claim', 'paste']) { + const w = watch(createWorkersApi({ getJwt, fetchImpl: slowAfter401() }).pultAction('p1', action, bodyFor(action))) + await flush() + await tick(REQUEST_TIMEOUT_MS) + assert.equal(w.state, 'fehler', action) + assert.deepEqual([w.error.message, w.error.status], [DOWN, 0], action) + } +}) + // Endprüfung P4 (W1, D14): eine Fehlerseite des Proxys (502/504) bei reply, approve-preview und start-build heißt nicht „nicht erreichbar“: // der Dienst arbeitet weiter, wenn nginx aufgibt, die Mail kann schon hinausgegangen sein. Der Client selbst (read) bleibt bei DOWN_TEXT // und dem echten Status; die Aktions-Meldung (actionErrorText) entscheidet nach Aktion und Status.