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>
148 lines
5.2 KiB
JavaScript
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)
|
|
})
|