Files
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00

542 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/<sub>', 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:<pm>', 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/<sub> 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)
})