Endprüfung P2. Ein Abruf auf toter Verbindung (Rechner im Ruhezustand, Netzwechsel) blieb offen, und die Feed-Schleifen laufen mit einem Abruf je Schleife: die Seite stand still und zeigte veraltete Daten als aktuell. createWorkersApi bricht jetzt jeden Versuch (JWT holen, senden, Antwort lesen) nach 20 s mit "Der Hustler-Dienst ist gerade nicht erreichbar." (status 0) ab, auch wenn fetch oder createJWT das Signal nicht beachten. Außerdem erzwang jede 401-Antwort ein frisches createJWT, bei dauerhaftem 401 etwa 16 bis 20 Mal je Minute und Tab. Appwrite begrenzt createJWT je Nutzer; das Limit hätte auch Serverpuls, Finanzen und Vorschauen gesperrt. Nach einer 401-Folge (frisches JWT wird ebenfalls abgelehnt oder lässt sich nicht erzeugen) gibt es 2 Minuten lang kein frisches JWT mehr, die Meldung "Anmeldung abgelaufen" bleibt; Erfolg hebt die Sperre auf. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
278 lines
13 KiB
JavaScript
278 lines
13 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { loadModule } from './load.mjs'
|
|
|
|
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
|
|
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS } = await loadModule('src/lib/workersApi.js')
|
|
|
|
const jwts = []
|
|
const getJwt = async (o) => {
|
|
jwts.push(o?.fresh === true)
|
|
return o?.fresh ? 'jwt-neu' : 'jwt-alt'
|
|
}
|
|
|
|
function fakeFetch(replies) {
|
|
const calls = []
|
|
const fn = async (url, init) => {
|
|
calls.push({ url, init })
|
|
const r = replies.shift()
|
|
if (r instanceof Error) throw r
|
|
return {
|
|
ok: r.status >= 200 && r.status < 300,
|
|
status: r.status,
|
|
json: async () => {
|
|
if (r.body === undefined) throw new SyntaxError('kein JSON')
|
|
return r.body
|
|
},
|
|
}
|
|
}
|
|
fn.calls = calls
|
|
return fn
|
|
}
|
|
|
|
const okReplies = (n) => Array.from({ length: n }, () => ({ status: 200, body: {} }))
|
|
|
|
test('URLs, Methoden, Header, Body', async () => {
|
|
const f = fakeFetch(okReplies(12))
|
|
const api = createWorkersApi({ getJwt, fetchImpl: f })
|
|
await api.state()
|
|
await api.events('2026-10-17T20:00:00.000+00:00')
|
|
await api.events(null, 500)
|
|
await api.trace('kim')
|
|
await api.pause(true)
|
|
await api.settings()
|
|
await api.saveSettings({ warnPct: 70 })
|
|
await api.pult()
|
|
await api.pultAction('p1', 'claim')
|
|
await api.botleads({ room: 'netz', q: 'Bäcker', page: 2 })
|
|
await api.botleads({ id: 'p1' })
|
|
await api.prospect('x/y')
|
|
assert.deepEqual(f.calls.map((c) => [c.init.method || 'GET', c.url]), [
|
|
['GET', '/api/workers/state'],
|
|
['GET', '/api/workers/events?after=2026-10-17T20%3A00%3A00.000%2B00%3A00&limit=200'],
|
|
['GET', '/api/workers/events?limit=200'],
|
|
['GET', '/api/workers/worker/kim/trace?limit=50'],
|
|
['POST', '/api/workers/pause'],
|
|
['GET', '/api/workers/settings'],
|
|
['PUT', '/api/workers/settings'],
|
|
['GET', '/api/workers/pult?status=offen'],
|
|
['POST', '/api/workers/pult/p1/claim'],
|
|
['GET', '/api/workers/botleads?room=netz&q=B%C3%A4cker&page=2'],
|
|
['GET', '/api/workers/botleads?id=p1'],
|
|
['GET', '/api/workers/prospect/x%2Fy'],
|
|
])
|
|
assert.equal(f.calls[0].init.headers['X-Appwrite-JWT'], 'jwt-alt')
|
|
assert.equal(f.calls[4].init.body, JSON.stringify({ paused: true }))
|
|
assert.equal(f.calls[4].init.headers['Content-Type'], 'application/json')
|
|
assert.equal(f.calls[6].init.body, JSON.stringify({ warnPct: 70 }))
|
|
})
|
|
|
|
test('Fehlertexte (Review Focus 1)', async () => {
|
|
const run = (reply) => createWorkersApi({ getJwt, fetchImpl: fakeFetch([reply]) }).state().then(() => null, (e) => [e.message, e.status])
|
|
assert.deepEqual(await run({ status: 200, body: undefined }), ['Der Hustler-Dienst antwortet nicht richtig.', 200]) // nginx liefert index.html
|
|
assert.deepEqual(await run({ status: 404, body: undefined }), ['Der Hustler-Dienst antwortet nicht richtig.', 404])
|
|
assert.deepEqual(await run({ status: 502, body: undefined }), ['Der Hustler-Dienst ist gerade nicht erreichbar.', 502])
|
|
assert.deepEqual(await run(new TypeError('fetch failed')), ['Der Hustler-Dienst ist gerade nicht erreichbar.', 0])
|
|
assert.deepEqual(await run({ status: 403, body: {} }), ['Dafür fehlt dir die Berechtigung.', 403])
|
|
assert.deepEqual(await run({ status: 400, body: { error: 'Tagesgrenze ungültig.' } }), ['Tagesgrenze ungültig.', 400])
|
|
assert.deepEqual(await run({ status: 500, body: {} }), ['Hustler-Fehler 500', 500])
|
|
await assert.rejects(
|
|
createWorkersApi({ getJwt: async () => { throw new Error('x') }, fetchImpl: fakeFetch([]) }).state(),
|
|
{ message: 'Anmeldung abgelaufen. Bitte neu anmelden.', status: 401 },
|
|
)
|
|
})
|
|
|
|
test('401 ⇒ einmal mit frischem JWT wiederholen, danach Meldung', async () => {
|
|
jwts.length = 0
|
|
const f = fakeFetch([{ status: 401, body: {} }, { status: 200, body: { ok: 1 } }])
|
|
assert.deepEqual(await createWorkersApi({ getJwt, fetchImpl: f }).state(), { ok: 1 })
|
|
assert.deepEqual(jwts, [false, true])
|
|
assert.equal(f.calls[1].init.headers['X-Appwrite-JWT'], 'jwt-neu')
|
|
await assert.rejects(
|
|
createWorkersApi({ getJwt, fetchImpl: fakeFetch([{ status: 401, body: {} }, { status: 401, body: {} }]) }).state(),
|
|
{ message: 'Anmeldung abgelaufen. Bitte neu anmelden.', status: 401 },
|
|
)
|
|
})
|
|
|
|
test('pult: ein Status, Komma-Liste oder Array (Übersicht §7); unbekannte Status werden abgewiesen, ohne Anfrage', async () => {
|
|
const f = fakeFetch(okReplies(5))
|
|
const api = createWorkersApi({ getJwt, fetchImpl: f })
|
|
await api.pult('offen,in_arbeit')
|
|
await api.pult(['gesendet', 'verworfen', 'erledigt'])
|
|
await api.pult(' veraltet ')
|
|
await api.pult('')
|
|
await api.pult('gesendet, verworfen , erledigt')
|
|
// Kommas bleiben unkodiert, Einträge getrimmt, leer = offen
|
|
assert.deepEqual(f.calls.map((c) => c.url), [
|
|
'/api/workers/pult?status=offen,in_arbeit',
|
|
'/api/workers/pult?status=gesendet,verworfen,erledigt',
|
|
'/api/workers/pult?status=veraltet',
|
|
'/api/workers/pult?status=offen',
|
|
'/api/workers/pult?status=gesendet,verworfen,erledigt',
|
|
])
|
|
for (const bad of ['offen,../x', 'quatsch', ['offen', 'x y'], 'offen;in_arbeit', 'offen,,in_arbeit']) {
|
|
await assert.rejects(api.pult(bad), /Unbekannter Pult-Status/)
|
|
}
|
|
assert.equal(f.calls.length, 5) // die fünf abgewiesenen Aufrufe haben nichts gesendet
|
|
})
|
|
|
|
test('pultAction: nur bekannte Aktionen, Pfad-Tricks abgewiesen', async () => {
|
|
const f = fakeFetch(okReplies(7))
|
|
const api = createWorkersApi({ getJwt, fetchImpl: f })
|
|
for (const bad of ['../x', 'delete', '']) {
|
|
await assert.rejects(api.pultAction('p1', bad), /Unbekannte Aktion/)
|
|
}
|
|
assert.equal(f.calls.length, 0)
|
|
for (const a of ['claim', 'sent', 'discard', 'done', 'reply', 'approve-preview', 'paste']) {
|
|
await api.pultAction('p1', a, a === 'reply' || a === 'paste' ? { text: 'x' } : undefined)
|
|
}
|
|
assert.deepEqual(f.calls.map((c) => c.url.split('/').pop()), ['claim', 'sent', 'discard', 'done', 'reply', 'approve-preview', 'paste'])
|
|
assert.equal(f.calls[4].init.body, JSON.stringify({ text: 'x' }))
|
|
// Plattform-Folge-Antwort einfügen (P4-T34)
|
|
assert.deepEqual([f.calls[6].url, f.calls[6].init.method, f.calls[6].init.body], ['/api/workers/pult/p1/paste', 'POST', JSON.stringify({ text: 'x' })])
|
|
})
|
|
|
|
test('limit wird geklemmt (Ereignisse 1..200, Spur 1..50)', async () => {
|
|
const f = fakeFetch(okReplies(3))
|
|
const api = createWorkersApi({ getJwt, fetchImpl: f })
|
|
await api.trace('kim', 999)
|
|
await api.events(null, 0)
|
|
await api.events(null, 150)
|
|
assert.deepEqual(f.calls.map((c) => c.url), [
|
|
'/api/workers/worker/kim/trace?limit=50',
|
|
'/api/workers/events?limit=1',
|
|
'/api/workers/events?limit=150',
|
|
])
|
|
})
|
|
|
|
// ---- Endprüfung P2: Zeitgrenze je Abruf und Sperre für frische JWTs nach einer 401-Folge ----
|
|
|
|
const DOWN = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
|
|
const SESSION = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
|
|
|
test('Zeitgrenze: ein hängender fetch endet mit der Dienst-Meldung (status 0), das Signal wird abgebrochen', async () => {
|
|
const signals = []
|
|
const hang = (url, init) => {
|
|
signals.push(init.signal)
|
|
return new Promise(() => {}) // beachtet das Signal nicht einmal: die Zeitgrenze muss trotzdem greifen
|
|
}
|
|
const api = createWorkersApi({ getJwt, fetchImpl: hang, timeoutMs: 30 })
|
|
const t0 = Date.now()
|
|
await assert.rejects(api.state(), { message: DOWN, status: 0 })
|
|
assert.ok(Date.now() - t0 < 2000)
|
|
assert.equal(signals.length, 1)
|
|
assert.equal(signals[0].aborted, true)
|
|
// jeder Aufruf hat seine eigene Zeitgrenze (hier: Schreibaufruf und Spur)
|
|
await assert.rejects(api.pause(true), { message: DOWN, status: 0 })
|
|
await assert.rejects(api.trace('kim'), { message: DOWN, status: 0 })
|
|
assert.equal(signals.length, 3)
|
|
})
|
|
|
|
test('Zeitgrenze: auch eine Antwort, deren Rumpf nie ankommt, und ein hängendes createJWT enden mit der Dienst-Meldung', async () => {
|
|
const stalledBody = async () => ({ ok: true, status: 200, json: () => new Promise(() => {}) })
|
|
await assert.rejects(createWorkersApi({ getJwt, fetchImpl: stalledBody, timeoutMs: 30 }).state(), { message: DOWN, status: 0 })
|
|
const hungJwt = () => new Promise(() => {})
|
|
const f = fakeFetch(okReplies(1))
|
|
await assert.rejects(createWorkersApi({ getJwt: hungJwt, fetchImpl: f, timeoutMs: 30 }).state(), { message: DOWN, status: 0 })
|
|
assert.equal(f.calls.length, 0) // ohne JWT ging nichts hinaus
|
|
})
|
|
|
|
test('Zeitgrenze: rechtzeitige Antworten bleiben unberührt, ein späterer Abruf klappt wieder (Schleife wird frei)', async () => {
|
|
let n = 0
|
|
const flaky = async () => {
|
|
n += 1
|
|
if (n === 1) return new Promise(() => {})
|
|
return { ok: true, status: 200, json: async () => ({ n }) }
|
|
}
|
|
const api = createWorkersApi({ getJwt, fetchImpl: flaky, timeoutMs: 40 })
|
|
await assert.rejects(api.state(), { message: DOWN, status: 0 })
|
|
assert.deepEqual(await api.state(), { n: 2 })
|
|
const quick = createWorkersApi({ getJwt, fetchImpl: fakeFetch(okReplies(1)) }) // Standard-Zeitgrenze 20 s: wird nach der Antwort aufgeräumt
|
|
assert.deepEqual(await quick.state(), {})
|
|
})
|
|
|
|
test('Zeitgrenze: Standardwerte 20 s und 2 min', () => {
|
|
assert.equal(REQUEST_TIMEOUT_MS, 20000)
|
|
assert.equal(AUTH_COOLDOWN_MS, 120000)
|
|
})
|
|
|
|
test('401-Folge: danach kein frisches JWT mehr, bis die Sperre abgelaufen ist (höchstens ein createJWT je Sperre)', async () => {
|
|
jwts.length = 0
|
|
let t = 1_000_000
|
|
const calls = []
|
|
const always401 = async (url, init) => {
|
|
calls.push(init.headers['X-Appwrite-JWT'])
|
|
return { ok: false, status: 401, json: async () => ({}) }
|
|
}
|
|
const api = createWorkersApi({ getJwt, fetchImpl: always401, now: () => t, authCooldownMs: 60000 })
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
assert.deepEqual(jwts, [false, true])
|
|
assert.deepEqual(calls, ['jwt-alt', 'jwt-neu']) // Versuch und Wiederholung
|
|
|
|
// innerhalb der Sperre: je Abruf genau eine Anfrage mit dem gespeicherten JWT, nie ein frisches, die Meldung bleibt
|
|
for (let i = 0; i < 20; i += 1) {
|
|
t += 2900 // 20 Schleifendurchläufe innerhalb der Sperre von 60 s (Zustand, Ereignisse und Spur zusammen)
|
|
await assert.rejects(api.events(null), { message: SESSION, status: 401 })
|
|
}
|
|
assert.equal(jwts.filter((fresh) => fresh).length, 1)
|
|
assert.equal(calls.length, 2 + 20)
|
|
|
|
// nach Ablauf der Sperre wird wieder einmal erneuert, danach beginnt die nächste Sperre
|
|
t += 2100
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
assert.equal(jwts.filter((fresh) => fresh).length, 2)
|
|
t += 5000
|
|
await assert.rejects(api.trace('kim'), { message: SESSION, status: 401 })
|
|
assert.equal(jwts.filter((fresh) => fresh).length, 2)
|
|
})
|
|
|
|
test('401-Folge: Erfolg hebt die Sperre auf, ein einzelnes 401 danach wird wieder mit frischem JWT wiederholt', async () => {
|
|
jwts.length = 0
|
|
let t = 0
|
|
const replies = [
|
|
{ status: 401, body: {} }, { status: 401, body: {} }, // Folge ⇒ Sperre
|
|
{ status: 200, body: { ok: 1 } }, // Dienst nimmt das gespeicherte JWT wieder an ⇒ Sperre weg
|
|
{ status: 401, body: {} }, { status: 200, body: { ok: 2 } }, // einzelnes 401 ⇒ Wiederholung mit frischem JWT hilft
|
|
]
|
|
const api = createWorkersApi({ getJwt, fetchImpl: fakeFetch(replies), now: () => t })
|
|
await assert.rejects(api.state(), { status: 401 })
|
|
t += 5000
|
|
assert.deepEqual(await api.state(), { ok: 1 })
|
|
assert.deepEqual(await api.state(), { ok: 2 })
|
|
assert.deepEqual(jwts, [false, true, false, false, true])
|
|
})
|
|
|
|
test('401-Folge: scheitert schon die Erzeugung des frischen JWT, beginnt dieselbe Sperre', async () => {
|
|
let t = 0
|
|
const mints = []
|
|
const brokenFresh = async (o) => {
|
|
mints.push(o?.fresh === true)
|
|
if (o?.fresh) throw new Error('429 Too Many Requests')
|
|
return 'jwt-alt'
|
|
}
|
|
const api = createWorkersApi({ getJwt: brokenFresh, fetchImpl: fakeFetch(Array.from({ length: 6 }, () => ({ status: 401, body: {} }))), now: () => t, authCooldownMs: 60000 })
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
t += 10000
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
t += 10000
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
assert.deepEqual(mints, [false, true, false, false]) // ein einziger Versuch, ein frisches JWT zu erzeugen
|
|
t += 60000
|
|
await assert.rejects(api.state(), { message: SESSION, status: 401 })
|
|
assert.deepEqual(mints, [false, true, false, false, false, true])
|
|
})
|
|
|
|
test('401-Folge: andere Fehler (403, 500, 502) heben die Sperre auf, weil der Dienst das JWT angenommen hat', async () => {
|
|
jwts.length = 0
|
|
let t = 0
|
|
const api = createWorkersApi({
|
|
getJwt,
|
|
fetchImpl: fakeFetch([{ status: 401, body: {} }, { status: 401, body: {} }, { status: 403, body: {} }, { status: 401, body: {} }, { status: 200, body: {} }]),
|
|
now: () => t,
|
|
})
|
|
await assert.rejects(api.state(), { status: 401 })
|
|
await assert.rejects(api.state(), { status: 403 })
|
|
assert.deepEqual(await api.state(), {}) // 401, Wiederholung frisch ⇒ 200
|
|
assert.deepEqual(jwts, [false, true, false, false, true])
|
|
})
|