Files
Webklar-Kundenbereich/test/repoMove.test.js
T
ClaudeandClaude Opus 5.5 405b2a0485 Umzug: Webhook-Adresse aus config.url, Aufräumen auch nach Selbstheilung
Fix round 1 (Review C1/I1/I2/I3, Controller-Ruling R11):
- C1 (kritisch): Hooks haben in echtem Gitea kein Top-Level-url, nur config.url -
  isInternalRepo/deletePortalHooks lasen bisher ein Feld, das es in Produktion nicht gibt.
- I1: Schritt-2-Namensvergleich jetzt ohne Ruecksicht auf Gross-/Kleinschreibung.
- I2: fs.rm in Schritt 9 gegen EACCES/EBUSY/ENOTEMPTY & Co. abgesichert.
- I3: Aufraeumen (Cache, alter Einzel-Webhook, Preview-Verzeichnis) laeuft jetzt ueber den
  neuen idempotenten finishMove()-Baustein in jedem Fall, in dem das Repo tatsaechlich in
  WEBklar liegt - auch nach einem Appwrite-Fehler, bei Selbstheilung und bei einem schon
  dort liegenden Repo (WARN.hookNotRemoved bei fehlgeschlagenem Loeschen).
- Fake Gitea: Hook-Form ohne Top-Level-url, 301 mit relativer Location (auch fuer
  Unterpfade) wie im echten Gitea 1.25.2, failNext() fuer gezielte Fehler in Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 10:21:34 +00:00

203 lines
8.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, before, after } from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { randomUUID } from 'node:crypto'
import { startFakeGitea } from './helpers/fakeGitea.js'
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
let gitea
let aw
let tmp
let originalCwd
let previewWorkDir
let moveRepoToOrg
let isOrgRepo
let isInternalRepo
let repoLockKey
let currentRepoFullName
// Schluessel: urspruenglich gesetzter repoFullName -> Appwrite-$id. Wird beim Seeden befuellt
// (siehe Controller-Ruling R2) - eine Live-Suche ueber repoFullName wuerde nach einem Umzug
// nichts mehr finden, weil sich das Feld dann geaendert hat.
const idByRepo = new Map()
/** Liefert das gespeicherte Dokument des Repos, ueber den zur Seed-Zeit gebauten Id-Index. */
function doc(name) {
return aw.get('websiteProjects', idByRepo.get(name))
}
const PORTAL_HOOK_URL = 'https://project.webklar.com/webhook/gitea'
before(async () => {
gitea = await startFakeGitea({
repos: [
{ full_name: 'JUSN/D-ner-Nation' },
{ full_name: 'knso/emmas-schatzkiste' },
{ full_name: 'ANDJ/Steuerungstechnik' },
{ full_name: 'knso/hytail-ark' },
{ full_name: 'WEBklar/schon-da' },
{ full_name: 'JUSN/schon-da' },
{ full_name: 'WEBklar/von-hand' },
{ full_name: 'ANDJ/unberuehrt' },
{ full_name: 'JUSN/doc-fail-test' },
{ full_name: 'JUSN/verboten' },
{ full_name: 'JUSN/Case-Test' },
],
// C1: Hooks kommen (wie im echten Gitea) OHNE Top-Level-`url` an - `url` hier ist nur die
// Seed-Abkuerzung, gespeichert/ausgeliefert wird das Ziel ausschliesslich unter `config.url`.
hooks: {
'knso/emmas-schatzkiste': [{ id: 110, url: PORTAL_HOOK_URL }],
'knso/hytail-ark': [{ id: 7, url: 'https://webklar.com/webhook/hytail-ark/deploy' }],
'WEBklar/von-hand': [{ id: 210, url: PORTAL_HOOK_URL }],
'WEBklar/schon-da': [{ id: 310, url: PORTAL_HOOK_URL }],
'JUSN/doc-fail-test': [{ id: 410, url: PORTAL_HOOK_URL }],
},
// knso/von-hand wurde von Hand bereits nach WEBklar/von-hand verschoben (kein Transfer-Aufruf noetig).
redirects: {
'knso/von-hand': 'WEBklar/von-hand',
},
})
const repoNames = [
'JUSN/D-ner-Nation',
'knso/emmas-schatzkiste',
'ANDJ/Steuerungstechnik',
'knso/hytail-ark',
'WEBklar/schon-da',
'JUSN/schon-da',
'knso/von-hand', // Appwrite kennt noch den alten Namen (siehe Brief)
'ANDJ/unberuehrt',
'JUSN/doc-fail-test',
'JUSN/verboten',
'jusn/case-test', // Appwrite kennt eine andere Schreibweise als Gitea (dort: JUSN/Case-Test)
]
const websiteProjects = repoNames.map((repoFullName) => {
const $id = randomUUID()
idByRepo.set(repoFullName, $id)
return { $id, repoFullName, projectName: repoFullName.split('/').pop() }
})
aw = await startFakeAppwrite({ seed: { websiteProjects } })
originalCwd = process.cwd()
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'repo-move-'))
process.chdir(tmp)
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
;({ previewWorkDir } = await import('../server/services/previewDeploy.js'))
;({ moveRepoToOrg, isOrgRepo, isInternalRepo, repoLockKey, currentRepoFullName } = await import(
'../server/services/repoMove.js'
))
})
after(async () => {
process.chdir(originalCwd)
await gitea.close()
await aw.close()
await fs.rm(tmp, { recursive: true, force: true })
})
test('verschiebt Repo aus eigenem Konto', async () => {
await fs.mkdir(previewWorkDir('JUSN/D-ner-Nation'), { recursive: true })
const r = await moveRepoToOrg(doc('JUSN/D-ner-Nation'))
assert.deepEqual(r, { moved: { from: 'JUSN/D-ner-Nation', to: 'WEBklar/D-ner-Nation' }, warnings: [] })
assert.deepEqual(gitea.calls.find((c) => c.path.endsWith('/transfer')).body, { new_owner: 'WEBklar' })
const d = aw.get('websiteProjects', doc('JUSN/D-ner-Nation').$id)
assert.equal(d.repoFullName, 'WEBklar/D-ner-Nation')
assert.equal(d.giteaRepoUrl, `${gitea.baseUrl}/WEBklar/D-ner-Nation`)
assert.equal(d.giteaRepoName, 'D-ner-Nation')
await assert.rejects(fs.access(previewWorkDir('JUSN/D-ner-Nation')))
})
test('löscht den alten Portal-Webhook', async () => {
const r = await moveRepoToOrg(doc('knso/emmas-schatzkiste'))
assert.deepEqual(r.moved, { from: 'knso/emmas-schatzkiste', to: 'WEBklar/emmas-schatzkiste' })
assert.deepEqual(gitea.hooksOf('WEBklar/emmas-schatzkiste'), [])
assert.equal(
gitea.calls.some(
(c) => c.method === 'DELETE' && c.path === '/api/v1/repos/WEBklar/emmas-schatzkiste/hooks/110'
),
true
)
})
test('docNotUpdated: Repo zieht trotzdem um, Portal-Webhook wird trotzdem geloescht', async () => {
aw.failNext({ method: 'PATCH', pathIncludes: doc('JUSN/doc-fail-test').$id, status: 500 })
const r = await moveRepoToOrg(doc('JUSN/doc-fail-test'))
assert.deepEqual(r.moved, { from: 'JUSN/doc-fail-test', to: 'WEBklar/doc-fail-test' })
assert.match(r.warnings[0], /^Das Repo liegt jetzt in WEBklar/)
assert.deepEqual(gitea.hooksOf('WEBklar/doc-fail-test'), [])
})
test('schon in WEBklar: nichts passiert', async () => {
const r = await moveRepoToOrg(doc('WEBklar/schon-da'))
assert.deepEqual(r, { moved: null, warnings: [] })
// R11: "Pushes nachtragen" raeumt auch bei einem schon in WEBklar liegenden Repo einen liegen
// gebliebenen Einzel-Webhook auf.
assert.deepEqual(gitea.hooksOf('WEBklar/schon-da'), [])
})
test('internes Repo per Liste und per fremdem Webhook', async () => {
for (const name of ['ANDJ/Steuerungstechnik', 'knso/hytail-ark']) {
assert.deepEqual(await moveRepoToOrg(doc(name)), { moved: null, warnings: ['Internes Repo, bleibt wo es ist.'] })
}
assert.equal(gitea.calls.filter((c) => c.path.endsWith('/transfer') && /steuerung|hytail/i.test(c.path)).length, 0)
})
test('Name existiert in WEBklar', async () => {
const r = await moveRepoToOrg(doc('JUSN/schon-da'))
assert.deepEqual(r, { moved: null, warnings: ['In WEBklar gibt es schon ein Repo namens schon-da. Bitte in Gitea umbenennen und dann ‚Pushes nachtragen‘ klicken.'] })
})
test('R10: 403 beim Transfer', async () => {
gitea.failNext({ method: 'POST', pathIncludes: '/transfer', status: 403 })
const r = await moveRepoToOrg(doc('JUSN/verboten'))
assert.equal(r.moved, null)
assert.match(r.warnings[0], /^Umzug nach WEBklar nicht möglich \(.+\)\.$/)
})
test('Weiterleitung korrigiert Projektdaten', async () => {
const r = await moveRepoToOrg(doc('knso/von-hand'))
assert.deepEqual(r.moved, { from: 'knso/von-hand', to: 'WEBklar/von-hand' })
assert.equal(gitea.calls.filter((c) => c.path.endsWith('/transfer') && c.path.includes('von-hand')).length, 0)
const d = aw.get('websiteProjects', doc('knso/von-hand').$id)
assert.equal(d.repoFullName, 'WEBklar/von-hand')
assert.equal(d.giteaRepoUrl, `${gitea.baseUrl}/WEBklar/von-hand`)
assert.equal(d.giteaRepoName, 'von-hand')
// R11/I3: der beim Hand-Umzug liegen gebliebene Einzel-Webhook wird jetzt mit aufgeraeumt.
assert.deepEqual(gitea.hooksOf('WEBklar/von-hand'), [])
})
test('nur Groß-/Kleinschreibung unterschiedlich: echter Umzug', async () => {
// I1: gespeichert ist "jusn/case-test", Gitea antwortet auf denselben Namen mit "JUSN/Case-Test"
// (reine Schreibweise, kein tatsaechlicher Umzug) - das darf den Selbstheilungs-Zweig NICHT
// ausloesen, sondern muss ganz normal weiter zum Transfer fuehren.
const r = await moveRepoToOrg(doc('jusn/case-test'))
assert.deepEqual(r, { moved: { from: 'jusn/case-test', to: 'WEBklar/Case-Test' }, warnings: [] })
assert.equal(
gitea.calls.some((c) => c.method === 'POST' && c.path === '/api/v1/repos/jusn/case-test/transfer'),
true
)
assert.equal(aw.get('websiteProjects', doc('jusn/case-test').$id).repoFullName, 'WEBklar/Case-Test')
})
test('Erkennung ohne Groß-/Kleinschreibung', async () => {
assert.equal(isOrgRepo('webklar/x'), true)
assert.equal(await isInternalRepo('andj/steuerungstechnik'), true)
})
test('repoLockKey', () => {
assert.equal(repoLockKey('JUSN/D-ner-Nation'), 'repo:jusn/d-ner-nation')
})
test('currentRepoFullName', async () => {
assert.equal(await currentRepoFullName('WEBklar/D-ner-Nation'), 'WEBklar/D-ner-Nation') // Eintrag vorhanden
assert.equal(await currentRepoFullName('JUSN/D-ner-Nation'), 'WEBklar/D-ner-Nation') // kein Eintrag, Weiterleitung
})
test('Gitea nicht erreichbar', async () => {
gitea.setDown(true)
const r = await moveRepoToOrg(doc('ANDJ/unberuehrt'))
gitea.setDown(false)
assert.equal(r.moved, null)
assert.match(r.warnings[0], /^Gitea ist gerade nicht erreichbar \(.+\)\. Bitte später ‚Pushes nachtragen‘ klicken\.$/)
})