Files
ClaudeandClaude Opus 5.5 d5eb908cb6 Preview-Router: öffentliche Seiten überstehen Server-Neustart
Nach einem Neustart startet der Kundenbereich oft vor Appwrite. Bisher
lieferte getPublicSubdomains dann ein leeres Set, syncPreviewTls setzte
alle Preview-Hosts hinter die Login-Weiche (preview-chain) und wiederholte
das nie. Öffentliche Seiten wie anelia-wolf und emmas-schatzkiste waren
bis zum nächsten Deploy nur noch mit Login erreichbar bzw. 403.

- Antwortet Appwrite nicht, übernimmt syncPreviewTls die öffentlichen
  Seiten aus der zuletzt geschriebenen preview-hosts.yml. Neue Subdomains
  bleiben login-geschützt, ohne alte Datei ist alles geschützt.
- Beim Start wird der Abgleich alle 15 s wiederholt (max. 10 min), bis er
  mit Appwrite-Daten geklappt hat.
- Tests: Datei lesen, Abgleich mit/ohne Appwrite, Startup-Wiederholung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 22:19:33 +00:00

148 lines
5.2 KiB
JavaScript

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