Files
Webklar-Kundenbereich/test/zeitenNeubau.test.js
T
ClaudeandClaude Opus 5.5 302b58cf72 Öffnungszeiten: Neubau anstoßen und Feld im Runner-Auftrag
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>
2026-10-05 01:41:10 +00:00

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'])
})