Messdienst: POST /a/admin/sites/:key/off setzt active=0 und offSince einmalig; Portal-Client stopSite. analyse.js: endAnalyse als gemeinsamer Kern, endAnalyseForCustomer, analyseOnProjectChanged, analyseDisabled (Sperre/Löschfrist) und täglicher Abgleich runAnalyseReconcile (Projekt/Kunde weg, Domains nachziehen, K18-Wiederholung, Messdienst für off/disabled, fehlende Entfernen-Tickets nachholen). Entfernen-Ticket je Abschaltung gekennzeichnet (kein Doppelticket, auch wenn das Speichern der ID scheiterte). Hooks: Kunde löschen, updateWebsiteProject, withdrawOrder (Wechsel), siteFeatures.disable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
145 lines
6.6 KiB
JavaScript
145 lines
6.6 KiB
JavaScript
import { test, before, after } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { startFakeGitea } from './helpers/fakeGitea.js'
|
|
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
|
|
|
let gitea
|
|
let aw
|
|
let config
|
|
let listAllAccessibleRepos
|
|
let importNewRepos
|
|
let syncReposToAppwrite
|
|
let isHostedProject
|
|
let ensureRepoPreviewConfig
|
|
|
|
before(async () => {
|
|
gitea = await startFakeGitea({
|
|
repos: [
|
|
{ full_name: 'knso/alt' },
|
|
{ full_name: 'JUSN/neu', description: 'Döner Nation' },
|
|
{ full_name: 'JUSN/archiv', archived: true },
|
|
{ full_name: 'knso/privat', private: true },
|
|
{ full_name: 'knso/synced-sub' },
|
|
{ full_name: 'knso/lead' },
|
|
],
|
|
})
|
|
|
|
aw = await startFakeAppwrite({
|
|
seed: {
|
|
websiteProjects: [
|
|
{ repoFullName: 'knso/alt', projectName: 'alt', subdomain: 'alt' },
|
|
{ repoFullName: 'knso/synced-sub', projectName: 'synced-sub', status: 'synced', subdomain: 'synced-sub' },
|
|
{ repoFullName: 'knso/lead', projectName: 'lead', status: 'deployed', subdomain: 'lead' },
|
|
{ $id: 'wp_an', repoFullName: 'knso/alt', projectName: 'Analyse-Projekt', subdomain: 'analyseprojekt', isPublic: true, projectType: 'website' },
|
|
],
|
|
portalFeatures: [
|
|
{ $id: 'wp_an_feat', customerId: 'cx', projectId: 'wp_an', featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'wpansitekey01234567' },
|
|
],
|
|
},
|
|
})
|
|
|
|
Object.assign(process.env, {
|
|
GITEA_BASE_URL: gitea.baseUrl,
|
|
GITEA_API_TOKEN: 'test-token',
|
|
APPWRITE_ENDPOINT: aw.endpoint,
|
|
APPWRITE_API_KEY: 'test-key',
|
|
PREVIEW_DEPLOY_ROOT: '',
|
|
TRAEFIK_DYNAMIC_DIR: '',
|
|
})
|
|
// config.js liest die Umgebung beim Import
|
|
;({ config } = await import('../server/config.js'))
|
|
;({ listAllAccessibleRepos, importNewRepos, syncReposToAppwrite, isHostedProject, ensureRepoPreviewConfig } =
|
|
await import('../server/services/giteaAdmin.js'))
|
|
})
|
|
|
|
after(async () => {
|
|
await gitea.close()
|
|
await aw.close()
|
|
})
|
|
|
|
test('importNewRepos legt nur fehlende an', async () => {
|
|
const r = await importNewRepos()
|
|
assert.equal(r.created, 2)
|
|
assert.deepEqual(r.createdRepos.map((x) => x.repo.full_name).sort(), ['JUSN/neu', 'knso/privat'])
|
|
assert.ok(r.createdRepos.every((x) => x.projectId))
|
|
const neu = aw.all('websiteProjects').find((d) => d.repoFullName === 'JUSN/neu')
|
|
assert.deepEqual([neu.projectName, neu.status, neu.subdomain || '', neu.previewUrl || ''], ['Döner Nation', 'synced', '', ''])
|
|
assert.ok(aw.all('websiteProjects').some((d) => d.repoFullName === 'knso/privat'))
|
|
assert.ok(!aw.all('websiteProjects').some((d) => d.repoFullName === 'JUSN/archiv'))
|
|
assert.equal(aw.calls.filter((c) => c.method === 'PATCH').length, 0)
|
|
})
|
|
test('importNewRepos läuft nie doppelt', async () => {
|
|
gitea.addRepo({ full_name: 'ANDJ/zweimal' })
|
|
await Promise.all([importNewRepos(), importNewRepos()])
|
|
assert.equal(aw.all('websiteProjects').filter((d) => d.repoFullName === 'ANDJ/zweimal').length, 1)
|
|
})
|
|
test('Abgleich vergibt keine Subdomain mehr', async () => {
|
|
gitea.addRepo({ full_name: 'ANDJ/sync-neu' })
|
|
await syncReposToAppwrite()
|
|
const d = aw.all('websiteProjects').find((x) => x.repoFullName === 'ANDJ/sync-neu')
|
|
assert.equal(d.subdomain || '', ''); assert.equal(d.previewUrl || '', '')
|
|
assert.equal(aw.all('websiteProjects').find((x) => x.repoFullName === 'knso/alt').subdomain, 'alt')
|
|
})
|
|
test('isHostedProject', () => {
|
|
for (const [doc, want] of [[{ projectType: 'preview' }, true], [{ projectType: 'website' }, true], [{ projectType: 'template' }, true],
|
|
[{ status: 'deployed' }, true], [{ status: 'pending' }, true], [{ status: 'synced', subdomain: 'x' }, false], [{}, false]]) {
|
|
assert.equal(isHostedProject(doc), want, JSON.stringify(doc))
|
|
}
|
|
})
|
|
test('Selbstheilung nur für gehostete Projekte', async () => {
|
|
assert.equal(await ensureRepoPreviewConfig('knso/synced-sub', 'main'), null)
|
|
assert.equal(gitea.calls.filter((c) => c.path.includes('synced-sub/contents')).length, 0)
|
|
const cfg = await ensureRepoPreviewConfig('knso/lead', 'main')
|
|
assert.equal(cfg.subdomain, 'lead')
|
|
assert.ok(gitea.calls.some((c) => c.method === 'POST' && c.path.includes('knso/lead/contents/.webklar-preview.json')))
|
|
})
|
|
test('Repo-Liste hat eine Obergrenze', async () => {
|
|
// R8: eigener Fake-Server + kurzzeitig umgebogene config.gitea.baseUrl (statt in `gitea` oben
|
|
// Tausende Repos zu seeden) - sonst wuerden importNewRepos/syncReposToAppwrite in den Tests
|
|
// darueber auf einmal Tausende zusaetzliche, unbenannte Repos mitverarbeiten.
|
|
const capRepos = []
|
|
for (let i = 0; i < 2005; i++) capRepos.push({ full_name: `cap/repo${i}` })
|
|
const capGitea = await startFakeGitea({ repos: capRepos })
|
|
const originalBaseUrl = config.gitea.baseUrl
|
|
const originalWarn = console.warn
|
|
console.warn = () => {}
|
|
config.gitea.baseUrl = capGitea.baseUrl
|
|
let repos
|
|
try {
|
|
repos = await listAllAccessibleRepos()
|
|
} finally {
|
|
config.gitea.baseUrl = originalBaseUrl
|
|
console.warn = originalWarn
|
|
await capGitea.close()
|
|
}
|
|
assert.equal(repos.length, 2000)
|
|
assert.equal(capGitea.calls.filter((c) => c.path === '/api/v1/repos/search').length, 40)
|
|
})
|
|
|
|
test('updateWebsiteProject: neue Subdomain => putSite mit neuer Domain; nicht öffentlich => stopSite; Hook-Fehler ändert das Ergebnis nicht', async () => {
|
|
const { updateWebsiteProject } = await import('../server/services/giteaAdmin.js')
|
|
const { analyseDeps } = await import('../server/services/analyse.js')
|
|
const calls = []
|
|
const real = analyseDeps.client
|
|
analyseDeps.client = {
|
|
putSite: async (k, b) => calls.push(['put', k, b]),
|
|
stopSite: async (k) => calls.push(['stop', k]),
|
|
}
|
|
try {
|
|
const r1 = await updateWebsiteProject('wp_an', { subdomain: 'analyseneu', projectType: 'website' })
|
|
assert.equal(r1.subdomain, 'analyseneu')
|
|
assert.deepEqual(calls, [['put', 'wpansitekey01234567', { projectId: 'wp_an', domains: ['analyseneu.project.webklar.com'], active: true }]])
|
|
calls.length = 0
|
|
const r2 = await updateWebsiteProject('wp_an', { projectType: 'preview' })
|
|
assert.equal(r2.isPublic, false)
|
|
assert.deepEqual(calls, [['stop', 'wpansitekey01234567']])
|
|
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
|
await updateDocument(config.collections.portalFeatures, 'wp_an_feat', { state: 'active', enabled: true })
|
|
analyseDeps.client = { putSite: async () => { throw new Error('weg') }, stopSite: async () => { throw new Error('weg') } }
|
|
const r3 = await updateWebsiteProject('wp_an', { subdomain: 'nochneu', projectType: 'website' })
|
|
assert.equal(r3.subdomain, 'nochneu')
|
|
} finally {
|
|
analyseDeps.client = real
|
|
}
|
|
})
|