Files
Webklar-Kundenbereich/test/giteaAdmin.test.js
T
ClaudeandClaude Opus 5.5 e43cdb667b Projekt-Ablauf im Ticketsystem: Anlegen, Archivieren, Import repariert
- Neues Projekt ohne Subdomain: subdomain null statt '' (Unique-Index
  subdomain_unique, jedes weitere scheiterte mit "Document with the
  requested ID already exists"); ebenso Typ "Internes Projekt"
- Anlegen unter derselben Repo-Sperre wie der Webhook repository/created;
  upsertWebsiteProjectByRepo macht aus einem 409 ein Update
- Gitea-Beschreibung = Anzeigename; Art (kind) beim Anlegen aus dem Ticket
- Reaktivieren setzt status zurueck (vorher blieb 'archived' stehen)
- Import: archivierte Repos als archiviert, Archiv-Stand aus Gitea wird
  gespiegelt, ein fehlerhaftes Repo bricht den Lauf nicht mehr ab
- Abgleich ueberschreibt Projektnamen nicht mehr
- CORS erlaubt PUT (Preview-Variablen speichern)
- Vorlage zieht beim Zuweisen nie in die Org um
- Erinnerungs-Lauf ueberspringt archivierte Projekte
- kaputte Umlaute (U+FFFD) in drei Fehlermeldungen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:22:53 +00:00

149 lines
6.8 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'))
// In Gitea archivierte Repos kommen als archiviertes Projekt an (sichtbar unter "Archiviert"),
// aber nicht in createdRepos (keine Zuordnungs-Mail)
const archiv = aw.all('websiteProjects').find((d) => d.repoFullName === 'JUSN/archiv')
assert.deepEqual([archiv.archived, archiv.status], [true, 'archived'])
assert.equal(r.archivedImported, 1)
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
}
})