239 lines
11 KiB
JavaScript
239 lines
11 KiB
JavaScript
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' },
|
||
{ full_name: 'knso/rausgezogen' },
|
||
],
|
||
// 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/rausgezogen': [{ id: 510, url: PORTAL_HOOK_URL }],
|
||
},
|
||
// knso/von-hand wurde von Hand bereits nach WEBklar/von-hand verschoben (kein Transfer-Aufruf noetig).
|
||
// WEBklar/rausgezogen wurde von Hand AUS der Organisation heraus nach knso/rausgezogen verschoben
|
||
// (final-fix Punkt 2: Schritt-1-Aufraeumen darf dieser Weiterleitung nicht folgen).
|
||
redirects: {
|
||
'knso/von-hand': 'WEBklar/von-hand',
|
||
'WEBklar/rausgezogen': 'knso/rausgezogen',
|
||
},
|
||
})
|
||
|
||
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)
|
||
'WEBklar/rausgezogen', // Appwrite kennt noch den WEBklar-Namen, Gitea hat es herausverschoben
|
||
]
|
||
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\.$/)
|
||
})
|
||
test('Schritt 1 folgt keiner Weiterleitung: von Hand aus WEBklar herausverschobenes Repo wird nicht angefasst', async () => {
|
||
// final-fix Punkt 2: WEBklar/rausgezogen (isOrgRepo(from) === true) leitet inzwischen auf
|
||
// knso/rausgezogen um - das Aufraeumen darf dem NICHT folgen (sonst wuerde der Portal-Webhook
|
||
// eines fremden Repos geloescht). Stattdessen wird der kanonische Name aufgeloest und, weil er
|
||
// ausserhalb von WEBklar liegt, nur das Projektdokument nachgezogen.
|
||
const r = await moveRepoToOrg(doc('WEBklar/rausgezogen'))
|
||
assert.deepEqual(r, { moved: null, warnings: [] })
|
||
assert.equal(
|
||
gitea.calls.some((c) => c.method === 'DELETE' && /rausgezogen/i.test(c.path)),
|
||
false
|
||
)
|
||
assert.deepEqual(gitea.hooksOf('knso/rausgezogen'), [
|
||
{ id: 510, type: 'gitea', active: true, events: ['push'], config: { content_type: 'json', url: PORTAL_HOOK_URL } },
|
||
])
|
||
const d = aw.get('websiteProjects', doc('WEBklar/rausgezogen').$id)
|
||
assert.equal(d.repoFullName, 'knso/rausgezogen')
|
||
})
|
||
test('R10-Ein-Zeiler: Transfer meldet Erfolg, aber unveraenderten full_name -> kein Aufraeumen', async () => {
|
||
// final-fix Punkt 3: der Transfer selbst antwortet 201, aber full_name ist danach nicht in
|
||
// WEBklar (simuliert einen Gitea-Bug/Teil-Fehlschlag). Erst NACH dieser Pruefung darf
|
||
// aufgeraeumt werden - hier eben nicht.
|
||
gitea.setTransferNoop()
|
||
const r = await moveRepoToOrg(doc('ANDJ/unberuehrt'))
|
||
assert.equal(r.moved, null)
|
||
assert.deepEqual(r.warnings, ['Umzug nach WEBklar nicht möglich (Übertragung nicht abgeschlossen).'])
|
||
assert.equal(
|
||
gitea.calls.some((c) => c.method === 'DELETE' && /unberuehrt/i.test(c.path)),
|
||
false
|
||
)
|
||
})
|