import { test, after, beforeEach } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { schemaKeys } from './support/schema.js' import { useTestEnv } from './support/testEnv.js' // Einbau des Zeiten-Skripts (Spec 4.6): buildZeiten entscheidet fuers Runner-Feld `zeiten`, stosseNeubauAn loest beim // Uebergang in einen ausgebenden Zustand genau einen Neubau aus (Portal-weit nacheinander) und meldet angestossen/manuell. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') }, defaultLimit: 25 }) after(() => aw.close()) useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) const { config } = await import('../server/config.js') const { buildZeiten } = await import('../server/services/zeiten/einbau.js') const { stosseNeubauAn, zeitenDeps } = await import('../server/services/zeiten/neubau.js') const KEY = 'abcdefghijklmnop' const feature = (state, projectId = 'p1', extra = {}) => aw.seed('portalFeatures', { $id: `f_${projectId}_zeiten`, customerId: 'c1', projectId, featureKey: 'zeiten', state, enabled: state === 'active', siteKey: KEY, ...extra }) const project = (extra = {}) => ({ $id: 'p1', customerId: 'c1', subdomain: 'bar-und-grill', repoFullName: 'JUSN/bar-und-grill', previewMode: 'auto', previewDetected: 'static', ...extra }) const original = zeitenDeps.rebuild const warnings = [] const warn = console.warn beforeEach(() => { zeitenDeps.rebuild = original warnings.length = 0 console.warn = (...args) => warnings.push(args.join(' ')) }) after(() => { zeitenDeps.rebuild = original console.warn = warn }) test('buildZeiten: setup und active liefern siteKey, off/ordered/disabled null', async () => { for (const state of ['setup', 'active']) { feature(state, `p_${state}`) assert.deepEqual(await buildZeiten(project({ $id: `p_${state}` })), { siteKey: KEY }, state) } for (const state of ['off', 'ordered', 'disabled']) { feature(state, `p_${state}`) assert.equal(await buildZeiten(project({ $id: `p_${state}` })), null, state) } }) test('buildZeiten: ohne Feature-Dokument, ohne siteKey, fremder Kunde oder ohne Kundenbezug null', async () => { assert.equal(await buildZeiten(project({ $id: 'p_ohne_doc' })), null) feature('active', 'p_ohne_key', { siteKey: '' }) assert.equal(await buildZeiten(project({ $id: 'p_ohne_key' })), null) feature('active', 'p_fremd') assert.equal(await buildZeiten(project({ $id: 'p_fremd', customerId: 'c_andere' })), null) assert.equal(await buildZeiten(project({ customerId: '' })), null) assert.equal(await buildZeiten(null), null) assert.equal(await buildZeiten(undefined), null) }) test('buildZeiten: isPublic ist nicht noetig (anders als bei der Analyse)', async () => { feature('active', 'p_privat') assert.deepEqual(await buildZeiten(project({ $id: 'p_privat', isPublic: false })), { siteKey: KEY }) }) test('buildZeiten: Sperrliste ⇒ null (auch bei anderer Gross-/Kleinschreibung), andere Subdomains nicht', async () => { assert.ok(config.zeiten.keinNeubau.includes('emmas-schatzkiste'), 'Vorgabe der Sperrliste') feature('active', 'p_emma') assert.equal(await buildZeiten(project({ $id: 'p_emma', subdomain: 'emmas-schatzkiste' })), null) assert.equal(await buildZeiten(project({ $id: 'p_emma', subdomain: 'Emmas-Schatzkiste' })), null) assert.deepEqual(await buildZeiten(project({ $id: 'p_emma', subdomain: 'emmas-schatzkiste-2' })), { siteKey: KEY }) }) test('buildZeiten wirft nie (Appwrite-Fehler ⇒ null, Warnung ohne Kundendaten)', async () => { feature('active', 'p_fehler') aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500 }) assert.equal(await buildZeiten(project({ $id: 'p_fehler' })), null) assert.equal(warnings.length, 1) assert.match(warnings[0], /p_fehler/) assert.doesNotMatch(warnings[0], new RegExp(`${KEY}|c1|bar-und-grill`)) // der naechste Aufruf klappt wieder assert.deepEqual(await buildZeiten(project({ $id: 'p_fehler' })), { siteKey: KEY }) }) test('Neubau: queued ⇒ angestossen, das Projekt geht an den Neubau', async () => { const gesehen = [] zeitenDeps.rebuild = async (p) => { gesehen.push(p) return { queued: true, runId: 'run-1', subdomain: p.subdomain } } const p = project() assert.equal(await stosseNeubauAn(p), 'angestossen') assert.deepEqual(gesehen, [p]) }) test('Neubau: nur queued === true zaehlt, alles andere ⇒ manuell', async () => { for (const result of [{ skipped: true, reason: 'Preview ist ausgeschaltet' }, { error: 'Runner kaputt', subdomain: 'x' }, { deployed: true }, { deployed: false, reason: 'PREVIEW_DEPLOY_ROOT nicht gesetzt' }, { queued: 'ja' }, { queued: 1 }, {}, null, undefined]) { zeitenDeps.rebuild = async () => result assert.equal(await stosseNeubauAn(project()), 'manuell', JSON.stringify(result)) } }) test('Neubau: kein Repo, Preview aus, Dev-Modus und Sperrliste ⇒ manuell, ohne Neubau', async () => { let aufrufe = 0 zeitenDeps.rebuild = async () => { aufrufe += 1 return { queued: true } } assert.equal(await stosseNeubauAn(project({ repoFullName: '' })), 'manuell', 'kein Repo') assert.equal(await stosseNeubauAn(project({ repoFullName: undefined })), 'manuell', 'Repo fehlt') assert.equal(await stosseNeubauAn(project({ previewMode: 'off' })), 'manuell', 'Preview aus') assert.equal(await stosseNeubauAn(project({ previewDetected: 'dev' })), 'manuell', 'Dev-Modus') assert.equal(await stosseNeubauAn(project({ previewDetected: 'dev (vite)' })), 'manuell', 'Dev-Modus mit Zusatz') assert.equal(await stosseNeubauAn(project({ subdomain: 'emmas-schatzkiste' })), 'manuell', 'Sperrliste') assert.equal(await stosseNeubauAn(project({ subdomain: 'EMMAS-Schatzkiste' })), 'manuell', 'Sperrliste, Schreibweise') assert.equal(await stosseNeubauAn(null), 'manuell', 'kein Projekt') assert.equal(aufrufe, 0) // Gegenprobe: ein normales Projekt (auch Vorgabemodus ohne previewMode) wird gebaut assert.equal(await stosseNeubauAn(project({ previewMode: undefined, previewDetected: undefined })), 'angestossen') assert.equal(aufrufe, 1) }) test('Neubau: 503 (Runner ausgelastet) und andere Fehler ⇒ manuell, wirft nie, Warnung ohne Kundendaten', async () => { zeitenDeps.rebuild = async () => { const err = new Error('Server ausgelastet, versuche es gleich noch mal') err.status = 503 err.expected = true throw err } assert.equal(await stosseNeubauAn(project()), 'manuell') zeitenDeps.rebuild = async () => { throw new Error('Netzwerk weg') } assert.equal(await stosseNeubauAn(project()), 'manuell') assert.equal(warnings.length, 2) for (const w of warnings) { assert.match(w, /p1/) assert.doesNotMatch(w, /bar-und-grill|c1\b/) } }) test('Neubauten laufen nacheinander (Sperre zeiten-neubau)', async () => { let laufend = 0 let maximal = 0 const reihenfolge = [] zeitenDeps.rebuild = async (p) => { laufend += 1 maximal = Math.max(maximal, laufend) reihenfolge.push(`start ${p.$id}`) await new Promise((resolve) => setTimeout(resolve, 15)) reihenfolge.push(`ende ${p.$id}`) laufend -= 1 return { queued: true } } const ergebnisse = await Promise.all(['a', 'b', 'c'].map((id) => stosseNeubauAn(project({ $id: id })))) assert.deepEqual(ergebnisse, ['angestossen', 'angestossen', 'angestossen']) assert.equal(maximal, 1, 'nie zwei Neubauten gleichzeitig') assert.deepEqual(reihenfolge, ['start a', 'ende a', 'start b', 'ende b', 'start c', 'ende c']) }) test('Neubau: ein gescheiterter Neubau haelt die Sperre nicht fest', async () => { let n = 0 zeitenDeps.rebuild = async () => { n += 1 if (n === 1) throw new Error('kaputt') return { queued: true } } const [a, b] = await Promise.all([stosseNeubauAn(project({ $id: 'a' })), stosseNeubauAn(project({ $id: 'b' }))]) assert.deepEqual([a, b], ['manuell', 'angestossen']) })