import { test, before, after } 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' // Nachgestellter Server-Neustart: Appwrite antwortet erst, wenn appwriteUp true ist. let appwriteUp = true const appwrite = http.createServer((req, res) => { if (!appwriteUp) return req.socket.destroy() // wie beim Boot: "fetch failed" res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ total: 1, documents: [{ subdomain: 'oeffentlich' }] })) }) let tmp let hostsFile let previewDeploy before(async () => { await new Promise((resolve) => appwrite.listen(0, '127.0.0.1', resolve)) tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'preview-sync-')) for (const name of ['oeffentlich', 'privat']) { await fs.mkdir(path.join(tmp, 'previews', name), { recursive: true }) } hostsFile = path.join(tmp, 'dynamic', 'preview-hosts.yml') Object.assign(process.env, { APPWRITE_ENDPOINT: `http://127.0.0.1:${appwrite.address().port}/v1`, APPWRITE_API_KEY: 'test-key', PREVIEW_DEPLOY_ROOT: path.join(tmp, 'previews'), TRAEFIK_DYNAMIC_DIR: path.join(tmp, 'dynamic'), }) // config.js liest die Umgebung beim Import previewDeploy = await import('../server/services/previewDeploy.js') }) after(async () => { appwrite.close() await fs.rm(tmp, { recursive: true, force: true }) }) const publicInFile = async () => [...previewDeploy.publicSubdomainsFromHostsFile(await fs.readFile(hostsFile, 'utf8'))].sort() /* ------------------------------------------------------------- Datei --- */ test('Datei lesen: nur HTTPS-Router ohne preview-chain gelten als öffentlich', () => { const content = [ '# Auto-generated by previewDeploy.syncPreviewTls - do not edit manually', 'http:', ' routers:', ' preview-a-b-https:', ' rule: "Host(`a-b.project.webklar.com`)"', ' service: preview-nginx@docker', ' priority: 100', '', ' preview-a-b-http:', ' middlewares:', ' - preview-sites-redirect@docker', ' preview-geheim-https:', ' service: preview-nginx@docker', ' middlewares:', ' - preview-chain@docker', ' preview-geheim-http:', ' priority: 100', ].join('\n') assert.deepEqual([...previewDeploy.publicSubdomainsFromHostsFile(content)], ['a-b']) assert.equal(previewDeploy.publicSubdomainsFromHostsFile('').size, 0) assert.equal(previewDeploy.publicSubdomainsFromHostsFile(undefined).size, 0) }) /* --------------------------------------------------------- Abgleich --- */ test('Abgleich mit Appwrite: öffentliche Seite ohne Login, Rest geschützt', async () => { appwriteUp = true const result = await previewDeploy.syncPreviewTls() assert.deepEqual(result, { synced: true, hosts: 2, publicSource: 'appwrite' }) assert.deepEqual(await publicInFile(), ['oeffentlich']) }) test('Neustart, Appwrite noch nicht da: öffentliche Seiten bleiben öffentlich, neue bleiben geschützt', async () => { appwriteUp = true await previewDeploy.syncPreviewTls() await fs.mkdir(path.join(tmp, 'previews', 'neu')) appwriteUp = false try { const result = await previewDeploy.syncPreviewTls() assert.deepEqual(result, { synced: true, hosts: 3, publicSource: 'previous-file' }) assert.deepEqual(await publicInFile(), ['oeffentlich']) assert.match(await fs.readFile(hostsFile, 'utf8'), /preview-neu-https:/) } finally { appwriteUp = true await fs.rm(path.join(tmp, 'previews', 'neu'), { recursive: true }) } }) test('Ohne Appwrite und ohne alte Datei: alles login-geschützt (sicherer Default)', async () => { await fs.rm(hostsFile, { force: true }) appwriteUp = false try { const result = await previewDeploy.syncPreviewTls() assert.equal(result.publicSource, 'previous-file') assert.deepEqual(await publicInFile(), []) } finally { appwriteUp = true } }) /* ---------------------------------------------------------- Startup --- */ test('Startup: wiederholt, bis der Abgleich mit Appwrite-Daten geklappt hat', async () => { const answers = [ { synced: true, hosts: 2, publicSource: 'previous-file' }, new Error('Schreiben fehlgeschlagen'), { synced: true, hosts: 2, publicSource: 'appwrite' }, ] let calls = 0 const sync = async () => { const answer = answers[calls++] if (answer instanceof Error) throw answer return answer } const result = await previewDeploy.syncPreviewTlsOnStartup({ sync, retryMs: 0 }) assert.equal(calls, 3) assert.equal(result.publicSource, 'appwrite') }) test('Startup: ohne Preview-Konfiguration kein Wiederholen', async () => { let calls = 0 const sync = async () => { calls++ return { synced: false, reason: 'PREVIEW_DEPLOY_ROOT oder TRAEFIK_DYNAMIC_DIR nicht gesetzt' } } await previewDeploy.syncPreviewTlsOnStartup({ sync, retryMs: 0 }) assert.equal(calls, 1) }) test('Startup: gibt nach maxAttempts auf', async () => { let calls = 0 const sync = async () => { calls++ return { synced: true, hosts: 2, publicSource: 'previous-file' } } const result = await previewDeploy.syncPreviewTlsOnStartup({ sync, retryMs: 0, maxAttempts: 4 }) assert.equal(calls, 4) assert.equal(result, null) })