buildZeiten (einbau.js) liefert dem Runner-Auftrag { siteKey } bei Funktion in setup/active,
null bei Sperrliste und Fehlern. stosseNeubauAn (neubau.js) stößt den Neubau unter der
Sperre zeiten-neubau an und meldet angestossen oder manuell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
7.8 KiB
JavaScript
167 lines
7.8 KiB
JavaScript
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'])
|
|
})
|