import { test, before, after, beforeEach } from 'node:test' import assert from 'node:assert/strict' import http from 'node:http' import fs from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import crypto from 'node:crypto' import { startFakeAppwrite } from './helpers/fakeAppwrite.js' let aw, runner, gitea, tmp, pd, logs, envLib const key = crypto.randomBytes(32) const SHA = 'a'.repeat(40) // Fake-Runner: zaehlt Aufrufe, Laeufe per Skript steuerbar const rs = { calls: [], runs: new Map(), next: 0, script: null, submitError: null } function startRunner() { return http.createServer((req, res) => { let raw = '' req.on('data', (c) => (raw += c)) req.on('end', () => { const body = raw ? JSON.parse(raw) : null rs.calls.push({ method: req.method, url: req.url, auth: req.headers.authorization, body }) res.setHeader('Content-Type', 'application/json') const send = (code, obj) => { res.statusCode = code; res.end(JSON.stringify(obj)) } if (req.method === 'POST' && req.url === '/jobs/build') { if (rs.submitError) return send(rs.submitError, { error: 'Server ausgelastet' }) const runId = `run-${++rs.next}` rs.runs.set(runId, rs.script(body)) return send(200, { runId }) } const m = req.url.match(/^\/jobs\/(.+)$/) if (m && req.method === 'GET') return rs.runs.has(m[1]) ? send(200, rs.runs.get(m[1])) : send(404, { error: 'unbekannt' }) if (req.method === 'PUT' && req.url.startsWith('/dev/')) return send(200, { state: 'sleeping' }) send(404, { error: 'x' }) }) }) } const done = (over = {}) => (job) => ({ state: 'done', detected: { kind: 'static', pm: 'npm' }, sha: job.sha, log: ['ok'], error: null, result: 'static', ...over }) const submits = () => rs.calls.filter((c) => c.url === '/jobs/build') before(async () => { aw = await startFakeAppwrite({ seed: { websiteProjects: [] } }) runner = startRunner() await new Promise((r) => runner.listen(0, '127.0.0.1', r)) gitea = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ commit: { id: 'b'.repeat(40) } })) }) await new Promise((r) => gitea.listen(0, '127.0.0.1', r)) tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'preview-runner-')) Object.assign(process.env, { APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key', PREVIEW_DEPLOY_ROOT: path.join(tmp, 'previews'), TRAEFIK_DYNAMIC_DIR: path.join(tmp, 'dynamic'), GITEA_BASE_URL: `http://127.0.0.1:${gitea.address().port}`, GITEA_API_TOKEN: 'tok', PREVIEW_RUNNER_URL: `http://127.0.0.1:${runner.address().port}`, PREVIEW_RUNNER_TOKEN: 'sekret', PREVIEW_ENV_KEY: key.toString('base64'), }) const { config } = await import('../server/config.js') config.previewRunner.enabled = true config.previewRunner.logDir = path.join(tmp, 'logs') pd = await import('../server/services/previewDeploy.js') pd.runnerTiming.pollMs = 10 pd.runnerTiming.maxMs = 2000 logs = await import('../server/services/previewLogs.js') envLib = await import('../server/services/previewEnv.js') globalThis.__cfg = config }) after(async () => { await new Promise((r) => runner.close(r)) await new Promise((r) => gitea.close(r)) await aw.close() await fs.rm(tmp, { recursive: true, force: true }) }) let n = 0 async function project(extra = {}) { const id = `proj${++n}` const repo = `JUSN/site-${n}` const { createDocument } = await import('../server/services/appwriteAdmin.js') const doc = await createDocument('websiteProjects', { repoFullName: repo, projectName: `S${n}`, ...extra }, id) return { id, repo, doc } } const cfg = (repo) => ({ subdomain: repo.split('/')[1], type: 'static', branch: 'main' }) beforeEach(() => { rs.calls = []; rs.runs = new Map(); rs.script = done(); rs.submitError = null globalThis.__cfg.previewRunner.enabled = true }) test('Schalter aus: kein Runner-Aufruf, alter Pfad', async () => { const { repo } = await project() globalThis.__cfg.previewRunner.enabled = false globalThis.__cfg.preview.deployRoot = '' const r = await pd.runDeploy(repo, 'main', cfg(repo)) globalThis.__cfg.preview.deployRoot = path.join(tmp, 'previews') assert.equal(r.deployed, false) assert.match(r.reason, /PREVIEW_DEPLOY_ROOT/) assert.equal(rs.calls.length, 0) }) test('fremde Subdomain (kopierte Config): kein Runner-Auftrag, Projekt unverändert', async () => { const original = await project({ subdomain: 'original-seite' }) const kopie = await project() const r = await pd.runDeploy(kopie.repo, 'main', { ...cfg(kopie.repo), subdomain: 'original-seite' }, { project: kopie.doc, sha: SHA }) assert.equal(r.blocked, true) assert.equal(r.owner, original.repo) assert.equal(submits().length, 0) assert.equal(aw.get('websiteProjects', kopie.id).previewState, undefined) // danach laeuft ein normaler Auftrag fuer dasselbe Repo wieder an const ok = await pd.runDeploy(kopie.repo, 'main', cfg(kopie.repo), { project: kopie.doc, sha: SHA }) assert.equal(ok.queued, true) await pd.waitForRunFollowers() }) test('gesperrter Aufruf waehrend eines laufenden Baus: der Verfolger bringt den Bau trotzdem online', async () => { await project({ subdomain: 'andere-seite' }) const { id, repo, doc } = await project() const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) assert.equal(r.queued, true) const blocked = await pd.runDeploy(repo, 'main', { ...cfg(repo), subdomain: 'andere-seite' }, { project: doc, sha: SHA }) assert.equal(blocked.blocked, true) await pd.waitForRunFollowers() assert.equal(aw.get('websiteProjects', id).previewState, 'online') assert.equal(submits().length, 1) }) test('an: Auftrag mit entschlüsselten Variablen, sofortige Rückgabe, Ergebnis online', async () => { const { id, repo, doc } = await project({ projectName: 'Mein Projekt', previewEnv: envLib.encryptEnv({ API_URL: 'https://x.test' }, key) }) const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) assert.equal(r.queued, true) assert.equal(r.deployed, false) assert.equal(r.runId, 'run-' + rs.next) assert.equal(aw.get('websiteProjects', id).previewState, 'building') const [sub] = submits() assert.equal(sub.auth, 'Bearer sekret') assert.deepEqual(sub.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'auto', displayName: 'Mein Projekt', env: { API_URL: 'https://x.test' }, analytics: null, zeiten: null }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'online') assert.equal(d.previewDetected, 'static') assert.equal(d.previewBuiltSha, SHA) assert.ok(d.previewBuiltAt) assert.equal(d.previewError, '') assert.equal((await logs.listRunLogs(id)).length, 1) }) test('displayName: previewConfig.displayName hat Vorrang vor projectName, sonst projectName', async () => { const a = await project({ projectName: 'Projektname' }) await pd.runDeploy(a.repo, 'main', { ...cfg(a.repo), displayName: 'Doener Nation' }, { project: a.doc, sha: SHA }) await pd.waitForRunFollowers() assert.equal(submits().at(-1).body.displayName, 'Doener Nation') const b = await project({ projectName: 'Projektname' }) await pd.runDeploy(b.repo, 'main', cfg(b.repo), { project: b.doc, sha: SHA }) await pd.waitForRunFollowers() assert.equal(submits().at(-1).body.displayName, 'Projektname') }) test('Sha fehlt: Branch-Kopf per Gitea-API', async () => { const { repo, doc } = await project() await pd.runDeploy(repo, 'main', cfg(repo), { project: doc }) await pd.waitForRunFollowers() assert.equal(submits()[0].body.sha, 'b'.repeat(40)) }) test('ohne project-Option: Dokument per Repo gefunden', async () => { const { id, repo } = await project({ previewMode: 'build' }) await pd.runDeploy(repo, 'main', cfg(repo), { sha: SHA }) await pd.waitForRunFollowers() assert.equal(submits()[0].body.mode, 'build') assert.equal(aw.get('websiteProjects', id).previewState, 'online') }) test('failed: Zustand failed, Fehler gespeichert, Folge-Stand nicht online', async () => { rs.script = done({ state: 'failed', result: null, error: 'Build fehlgeschlagen', detected: { kind: 'build', pm: 'pnpm' }, log: ['boom'] }) const { id, repo, doc } = await project({ previewBuiltSha: 'c'.repeat(40), previewState: 'online' }) await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'failed') assert.equal(d.previewError, 'Build fehlgeschlagen') assert.equal(d.previewBuiltSha, 'c'.repeat(40)) assert.equal(await fs.stat(path.join(tmp, 'previews', repo.split('/')[1])).catch(() => null), null) }) test('dev: sleeping + Registrierung per PUT /dev/', async () => { rs.script = done({ result: 'dev', detected: { kind: 'dev', pm: 'pnpm' } }) const { id, repo, doc } = await project() await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'sleeping') assert.equal(d.previewBranch, 'main') assert.equal(d.previewBuiltSha, SHA) assert.equal(d.previewDetected, 'dev:pnpm') const put = rs.calls.find((c) => c.method === 'PUT') assert.equal(put.url, `/dev/${repo.split('/')[1]}`) assert.equal(put.body.sha, SHA) }) test('build: previewDetected build:', async () => { rs.script = done({ result: 'build', detected: { kind: 'build', pm: 'yarn' } }) const { id, repo, doc } = await project() await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() assert.equal(aw.get('websiteProjects', id).previewDetected, 'build:yarn') }) test('Modus off: kein Auftrag, previewState off', async () => { const { id, repo, doc } = await project({ previewMode: 'off' }) const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) assert.equal(r.skipped, true) assert.equal(rs.calls.length, 0) assert.equal(aw.get('websiteProjects', id).previewState, 'off') }) test('Protokoll maskiert und nur 5 behalten', async () => { const env = { TOKEN: 'supergeheim' } const id = 'logproj' const files = [] for (let i = 0; i < 7; i++) files.push(await logs.saveRunLog(id, [`Zeile ${i}`, 'nutzt supergeheim hier'], env)) const list = await logs.listRunLogs(id) assert.equal(list.length, 5) assert.equal(list[0].runFile, files[6].toString()) const text = await logs.readRunLog(id, list[0].runFile) assert.ok(!text.includes('supergeheim')) assert.ok(text.includes('•••')) assert.equal(await logs.readRunLog(id, files[0]), null) }) test('Protokoll: nur die letzten 300 Zeilen, keine Pfadtricks', async () => { const lines = Array.from({ length: 400 }, (_, i) => `z${i}`) const f = await logs.saveRunLog('logproj2', lines, {}) const text = (await logs.readRunLog('logproj2', f)).trim().split('\n') assert.equal(text.length, 300) assert.equal(text[0], 'z100') assert.equal(await logs.readRunLog('logproj2', '../../x.log'), null) assert.equal(await logs.readRunLog('logproj2', '../logproj/' + f), null) assert.equal(await logs.readRunLog('..', f), null) assert.deepEqual(await logs.listRunLogs('..'), []) }) test('Variablen im Lauf-Protokoll maskiert (Ende-zu-Ende)', async () => { rs.script = done({ log: ['Schluessel=supergeheim123'] }) const { id, repo, doc } = await project({ previewEnv: envLib.encryptEnv({ K: 'supergeheim123' }, key) }) await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() const [{ runFile }] = await logs.listRunLogs(id) assert.equal((await logs.readRunLog(id, runFile)).trim(), 'Schluessel=•••') }) test('neuerer Lauf verdrängt den älteren Verfolger', async () => { // Lauf 1 bleibt lange "running", Lauf 2 ist sofort fertig const { id, repo, doc } = await project() rs.script = () => ({ state: 'running', detected: null, sha: SHA, log: [], error: null, result: null }) const r1 = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) rs.script = done({ log: ['zwei'] }) const sha2 = 'd'.repeat(40) const r2 = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: sha2 }) assert.notEqual(r1.runId, r2.runId) // Lauf 1 meldet jetzt "failed" - darf nichts mehr schreiben rs.runs.set(r1.runId, { state: 'failed', error: 'alt', log: ['eins'], detected: null, sha: SHA, result: null }) await pd.waitForRunFollowers() const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'online') assert.equal(d.previewBuiltSha, sha2) assert.equal((await logs.listRunLogs(id)).length, 1) }) test('PREVIEW_ENV_KEY fehlt bei gespeicherten Variablen: Build ohne Variablen, Fehlertext', async () => { const { id, repo, doc } = await project({ previewEnv: envLib.encryptEnv({ A: 'xxxxxx' }, key) }) const saved = process.env.PREVIEW_ENV_KEY delete process.env.PREVIEW_ENV_KEY try { await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() } finally { process.env.PREVIEW_ENV_KEY = saved } assert.deepEqual(submits()[0].body.env, {}) const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'online') assert.equal(d.previewError, 'Preview-Variablen nicht entschlüsselbar (PREVIEW_ENV_KEY fehlt)') }) test('Runner ausgelastet (503): failed mit Meldung, kein Wurf', async () => { rs.submitError = 503 const { id, repo, doc } = await project() const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) assert.equal(r.deployed, false) assert.equal(r.error, 'Server ausgelastet') assert.equal(aw.get('websiteProjects', id).previewState, 'failed') }) test('Modus off während ein Lauf läuft: Zustand bleibt off, kein TLS-Sync vom alten Verfolger', async () => { const { id, repo, doc } = await project() rs.script = () => ({ state: 'running', detected: null, sha: SHA, log: [], error: null, result: null }) const r1 = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) const { updateDocument } = await import('../server/services/appwriteAdmin.js') const off = await updateDocument('websiteProjects', id, { previewMode: 'off' }) await pd.runDeploy(repo, 'main', cfg(repo), { project: off, sha: SHA }) rs.runs.set(r1.runId, { state: 'done', detected: { kind: 'static', pm: 'npm' }, sha: SHA, log: ['alt'], error: null, result: 'static' }) await pd.waitForRunFollowers() assert.equal(aw.get('websiteProjects', id).previewState, 'off') assert.equal((await logs.listRunLogs(id)).length, 0) }) test('gleichzeitige Aufrufe: der neueste gewinnt', async () => { const { id, repo, doc } = await project() rs.script = (job) => ({ state: 'done', detected: { kind: 'static', pm: 'npm' }, sha: job.sha, log: [job.sha], error: null, result: 'static' }) const sha2 = 'e'.repeat(40) const [, r2] = await Promise.all([ pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }), pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: sha2 }), ]) await pd.waitForRunFollowers() assert.ok(r2.queued) assert.equal(aw.get('websiteProjects', id).previewBuiltSha, sha2) }) test('fehlgeschlagener neuer Auftrag verdrängt den alten Verfolger', async () => { const { id, repo, doc } = await project() rs.script = () => ({ state: 'running', detected: null, sha: SHA, log: [], error: null, result: null }) const r1 = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) rs.submitError = 503 await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) rs.runs.set(r1.runId, { state: 'done', detected: { kind: 'static' }, sha: SHA, log: [], error: null, result: 'static' }) await pd.waitForRunFollowers() assert.equal(aw.get('websiteProjects', id).previewState, 'failed') }) test('R15: folgt queued/running über die alte 16-Minuten-Grenze, Runner-Ausfall lange toleriert', async () => { const { id, repo, doc } = await project() let polls = 0 rs.script = () => ({ state: 'queued', detected: null, sha: SHA, log: [], error: null, result: null }) const r = await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) const orig = rs.runs.get(r.runId) Object.defineProperty(orig, 'state', { get() { polls++; return polls < 15 ? 'queued' : 'done' }, enumerable: true }) await pd.waitForRunFollowers() assert.ok(polls >= 15) assert.equal(aw.get('websiteProjects', id).previewState, 'online') }) test('R15: Gesamtzeit überschritten => failed mit "Status unbekannt"', async () => { const { id, repo, doc } = await project() rs.script = () => ({ state: 'running', detected: null, sha: SHA, log: [], error: null, result: null }) const saved = pd.runnerTiming.maxMs pd.runnerTiming.maxMs = 60 try { await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() } finally { pd.runnerTiming.maxMs = saved } const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'failed') assert.equal(d.previewError, 'Status unbekannt – bitte neu bauen') }) test('R15: Runner dauerhaft unerreichbar (> unreachableMs) => failed', async () => { const { id, repo, doc } = await project() rs.script = () => ({ state: 'running', detected: null, sha: SHA, log: [], error: null, result: null }) const { config } = await import('../server/config.js') const savedUrl = config.previewRunner.url const saved = pd.runnerTiming.unreachableMs pd.runnerTiming.unreachableMs = 50 try { await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) config.previewRunner.url = 'http://127.0.0.1:1' await pd.waitForRunFollowers() } finally { config.previewRunner.url = savedUrl; pd.runnerTiming.unreachableMs = saved } const d = aw.get('websiteProjects', id) assert.equal(d.previewState, 'failed') assert.match(d.previewError, /nicht erreichbar/) }) test('Runner-Fehlertext wird maskiert', async () => { rs.script = done({ state: 'failed', result: null, error: 'Fehler mit supergeheim99', log: [] }) const { id, repo, doc } = await project({ previewEnv: envLib.encryptEnv({ K: 'supergeheim99' }, key) }) await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() assert.equal(aw.get('websiteProjects', id).previewError, 'Fehler mit •••') }) test('dev-Ergebnis: PUT /dev/ mit vollständigem Auftrag inkl. Variablen', async () => { rs.script = done({ result: 'dev', detected: { kind: 'dev', pm: 'npm' } }) const { repo, doc } = await project({ previewEnv: envLib.encryptEnv({ A: 'wert1234' }, key), previewMode: 'dev' }) await pd.runDeploy(repo, 'main', cfg(repo), { project: doc, sha: SHA }) await pd.waitForRunFollowers() const put = rs.calls.find((c) => c.method === 'PUT') assert.deepEqual(put.body, { sub: repo.split('/')[1], repo, sha: SHA, branch: 'main', mode: 'dev', env: { A: 'wert1234' } }) }) // ---- Analyse-Feld im Build-Auftrag (Task 10) ---- async function analyseProject({ feature = {}, customer = {}, project: pextra = {}, noFeature = false, noCustomer = false } = {}) { const { createDocument } = await import('../server/services/appwriteAdmin.js') const cid = `cust-an-${n + 1}` const p = await project({ customerId: cid, subdomain: `an-${n + 1}`, isPublic: true, ...pextra }) if (!noCustomer) { await createDocument('customers', { name: 'Max Muster', companyName: 'Muster GmbH', email: 'info@muster.test', street: 'Hauptstr. 1', postalCode: '12345', city: 'Berlin', ...customer }, cid) } if (!noFeature) { await createDocument('portalFeatures', { customerId: cid, projectId: p.id, featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'abcdef0123456789abcd', accentColor: '#ff0066', ...feature }, `f_${p.id}_analyse`) } return p } async function submitFor(p) { await pd.runDeploy(p.repo, 'main', cfg(p.repo), { project: p.doc, sha: SHA }) await pd.waitForRunFollowers() return submits().at(-1).body } test('Analyse: aktiv + öffentlich ⇒ analytics mit siteKey, Farbe, Datenschutzangaben', async () => { const body = await submitFor(await analyseProject()) assert.deepEqual(body.analytics, { siteKey: 'abcdef0123456789abcd', color: '#ff0066', privacy: { company: 'Muster GmbH', address: 'Hauptstr. 1, 12345 Berlin', email: 'info@muster.test' }, }) }) test('Analyse: Standardfarbe, Firma fällt auf Name zurück, Adresse optional', async () => { const p = await analyseProject({ feature: { accentColor: '' }, customer: { companyName: '', street: '', postalCode: '', city: '' } }) const body = await submitFor(p) assert.equal(body.analytics.color, '#111111') assert.equal(body.analytics.privacy.company, 'Max Muster') assert.equal(body.analytics.privacy.address, '') }) test('Analyse: ungültige E-Mail ⇒ null, nur Name reicht als Firma', async (t) => { t.mock.method(console, 'warn', () => {}) assert.equal((await submitFor(await analyseProject({ customer: { email: 'abc' } }))).analytics, null) const body = await submitFor(await analyseProject({ customer: { companyName: '', name: 'Nur Name' } })) assert.equal(body.analytics.privacy.company, 'Nur Name') }) test('Analyse: aus ⇒ null', async () => { assert.equal((await submitFor(await analyseProject({ feature: { state: 'off', enabled: false } }))).analytics, null) assert.equal((await submitFor(await analyseProject({ noFeature: true }))).analytics, null) }) test('Analyse: nicht öffentlich oder ohne Subdomain ⇒ null', async () => { assert.equal((await submitFor(await analyseProject({ project: { isPublic: false } }))).analytics, null) assert.equal((await submitFor(await analyseProject({ project: { subdomain: '' } }))).analytics, null) }) test('Analyse: fehlende E-Mail oder Firma ⇒ null mit Warnung, Build läuft trotzdem', async (t) => { const warn = t.mock.method(console, 'warn', () => {}) const a = await analyseProject({ customer: { email: '' } }) assert.equal((await submitFor(a)).analytics, null) assert.ok(warn.mock.calls.some((c) => String(c.arguments[0]).includes(a.id))) assert.equal((await submitFor(await analyseProject({ customer: { companyName: '', name: '' } }))).analytics, null) assert.equal((await submitFor(await analyseProject({ noCustomer: true }))).analytics, null) }) test('Analyse: Fehler bei der Funktions-Abfrage ⇒ null, Auftrag wird trotzdem gesendet', async (t) => { t.mock.method(console, 'warn', () => {}) const p = await analyseProject() aw.failNext({ method: 'GET', pathIncludes: 'portalFeatures', status: 500 }) const before = submits().length const body = await submitFor(p) assert.equal(submits().length, before + 1) assert.equal(body.analytics, null) }) test('Analyse: Dev-PUT nach erfolgreichem Build (Modus dev) enthält kein analytics', async () => { rs.script = done({ result: 'dev' }) const p = await analyseProject({ project: { previewMode: 'dev' } }) const body = await submitFor(p) assert.ok(body.analytics) const puts = rs.calls.filter((c) => c.method === 'PUT' && c.url.startsWith('/dev/')) assert.ok(puts.length >= 1) for (const c of puts) assert.equal('analytics' in c.body, false) }) // ---- Zeiten-Feld im Build-Auftrag (Oeffnungszeiten live, Spec 4.6) ---- async function zeitenProject({ state = 'active', project: pextra = {}, noFeature = false } = {}) { const { createDocument } = await import('../server/services/appwriteAdmin.js') const cid = `cust-ze-${n + 1}` const p = await project({ customerId: cid, subdomain: `ze-${n + 1}`, ...pextra }) if (!noFeature) { await createDocument('portalFeatures', { customerId: cid, projectId: p.id, featureKey: 'zeiten', state, enabled: state === 'active', siteKey: 'abcdefghijklmnop' }, `f_${p.id}_zeiten`) } return p } test('Zeiten: setup und active ⇒ zeiten mit siteKey, auch ohne isPublic', async () => { for (const state of ['setup', 'active']) { assert.deepEqual((await submitFor(await zeitenProject({ state }))).zeiten, { siteKey: 'abcdefghijklmnop' }, state) } }) test('Zeiten: off, ordered, disabled und ohne Funktion ⇒ null', async () => { for (const state of ['off', 'ordered', 'disabled']) { assert.equal((await submitFor(await zeitenProject({ state }))).zeiten, null, state) } assert.equal((await submitFor(await zeitenProject({ noFeature: true }))).zeiten, null) }) test('Zeiten: Sperrliste ⇒ null, Analyse bleibt unberührt', async () => { const body = await submitFor(await zeitenProject({ project: { subdomain: 'emmas-schatzkiste' } })) assert.equal(body.zeiten, null) assert.equal(body.analytics, null) }) test('Zeiten: Fehler bei der Funktions-Abfrage ⇒ null, Auftrag wird trotzdem gesendet', async (t) => { t.mock.method(console, 'warn', () => {}) const p = await zeitenProject() aw.failNext({ method: 'GET', pathIncludes: 'portalFeatures', status: 500 }) const before = submits().length const body = await submitFor(p) assert.equal(submits().length, before + 1) assert.equal(body.zeiten, null) }) test('Zeiten: Dev-PUT nach erfolgreichem Build (Modus dev) enthält kein zeiten', async () => { rs.script = done({ result: 'dev' }) const body = await submitFor(await zeitenProject({ project: { previewMode: 'dev' } })) assert.deepEqual(body.zeiten, { siteKey: 'abcdefghijklmnop' }) const puts = rs.calls.filter((c) => c.method === 'PUT' && c.url.startsWith('/dev/')) assert.ok(puts.length >= 1) for (const c of puts) assert.equal('zeiten' in c.body, false) }) test('Analyse: überlange Angaben, Steuerzeichen, schlechte Farbe ⇒ Grenzen eingehalten, Auftrag geht raus', async (t) => { t.mock.method(console, 'warn', () => {}) const p = await analyseProject({ feature: { accentColor: 'rot' }, customer: { companyName: 'F\u0007irma ' + 'x'.repeat(250), street: 's'.repeat(400) }, }) const a = (await submitFor(p)).analytics assert.equal(a.color, '#111111') assert.ok(a.privacy.company.length <= 200 && !/[\x00-\x1f]/.test(a.privacy.company)) assert.ok(a.privacy.address.length <= 300) const q = await analyseProject({ customer: { email: 'a'.repeat(250) + '@x.de' } }) assert.equal((await submitFor(q)).analytics, null) const r = await analyseProject({ customer: { email: 'keine-mail' } }) assert.equal((await submitFor(r)).analytics, null) })