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>
This commit is contained in:
1 parent
ba9916cfe6
commit
d5eb908cb6
3 files changed
+218
-10
No files matched your search
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
})
|
||||
Reference in new issue
Block a user