Files
Webklar-Kundenbereich/test/repoMove.test.js
T

239 lines
11 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' },
{ 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
)
})