Schnittstellen für Import, Zuweisen, Nachtragen; Webhook wartet auf Umzug
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5c39640224
commit
a5da4849e5
4 files changed
+338
-20
No files matched your search
@@ -1,5 +1,5 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { syncReposToAppwrite, fetchRepoReadme } from '../../services/giteaAdmin.js'
|
import { syncReposToAppwrite, fetchRepoReadme, importNewRepos } from '../../services/giteaAdmin.js'
|
||||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
@@ -16,6 +16,18 @@ router.post('/sync-repos', async (_req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Neue Repos aus Gitea als minimale Projektdokumente anlegen (vom Projekt-Dialog im
|
||||||
|
// Ticketsystem bei jedem Oeffnen aufgerufen).
|
||||||
|
router.post('/import-repos', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await importNewRepos()
|
||||||
|
return res.json({ ok: true, ...result })
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.status || 500
|
||||||
|
return res.status(status).json({ error: err.message || 'Import fehlgeschlagen' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
router.get('/repo-readme', async (req, res) => {
|
router.get('/repo-readme', async (req, res) => {
|
||||||
const repoFullName = String(req.query.repoFullName || '').trim()
|
const repoFullName = String(req.query.repoFullName || '').trim()
|
||||||
if (!repoFullName) {
|
if (!repoFullName) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
updateWebsiteProject,
|
updateWebsiteProject,
|
||||||
archiveRepo,
|
archiveRepo,
|
||||||
} from '../../services/giteaAdmin.js'
|
} from '../../services/giteaAdmin.js'
|
||||||
|
import { assignProject, reconcileProject } from '../../services/projectAssignment.js'
|
||||||
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
@@ -67,4 +68,40 @@ router.post('/:id/unarchive', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Projekt einem Kunden/Ticket zuweisen -> stoesst Umzug nach GITEA_ORG + Nachtragen an
|
||||||
|
router.post('/:id/assign', async (req, res) => {
|
||||||
|
const { customerId, ticketId } = req.body || {}
|
||||||
|
if (!customerId && !ticketId) {
|
||||||
|
return res.status(400).json({ error: 'customerId oder ticketId erforderlich' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await assignProject(req.params.id, {
|
||||||
|
customerId: String(customerId || ''),
|
||||||
|
ticketId: String(ticketId || ''),
|
||||||
|
})
|
||||||
|
return res.json({ ok: true, ...result })
|
||||||
|
} catch (err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
||||||
|
}
|
||||||
|
const status = err.status || 500
|
||||||
|
return res.status(status).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Umzug + Pushes eines bereits zugewiesenen Projekts erneut nachtragen
|
||||||
|
router.post('/:id/pushes', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await reconcileProject(req.params.id)
|
||||||
|
return res.json({ ok: true, ...result })
|
||||||
|
} catch (err) {
|
||||||
|
if (err.status === 404) {
|
||||||
|
return res.status(404).json({ error: 'Projekt nicht gefunden' })
|
||||||
|
}
|
||||||
|
const status = err.status || 500
|
||||||
|
return res.status(status).json({ error: err.message })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import { config } from '../../config.js'
|
import { config } from '../../config.js'
|
||||||
import { upsertWebsiteProjectByRepo } from '../../services/appwriteAdmin.js'
|
import { upsertWebsiteProjectByRepo, getDocument } from '../../services/appwriteAdmin.js'
|
||||||
import {
|
import {
|
||||||
fetchPreviewConfig,
|
fetchPreviewConfig,
|
||||||
defaultPreviewConfig,
|
defaultPreviewConfig,
|
||||||
@@ -12,6 +12,8 @@ import { ensureRepoPreviewConfig } from '../../services/giteaAdmin.js'
|
|||||||
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
import { createGitPushWorksheet } from '../../services/gitWorksheet.js'
|
||||||
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
import { ensureProjectTicket } from '../../services/ticketAdmin.js'
|
||||||
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
import { invalidateProjectCache } from '../../services/projectInsights.js'
|
||||||
|
import { withLocks } from '../../services/keyedLock.js'
|
||||||
|
import { repoLockKey, currentRepoFullName } from '../../services/repoMove.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
@@ -44,36 +46,45 @@ router.post('/gitea', async (req, res) => {
|
|||||||
const payload = req.body || {}
|
const payload = req.body || {}
|
||||||
const ref = payload.ref || ''
|
const ref = payload.ref || ''
|
||||||
const repo = payload.repository || {}
|
const repo = payload.repository || {}
|
||||||
const repoFullName = repo.full_name || ''
|
const payloadName = repo.full_name || ''
|
||||||
const commits = payload.commits || []
|
const commits = payload.commits || []
|
||||||
const pusher = payload.pusher || {}
|
const pusher = payload.pusher || {}
|
||||||
const commitSha = payload.after || commits[commits.length - 1]?.id || ''
|
const commitSha = payload.after || commits[commits.length - 1]?.id || ''
|
||||||
|
|
||||||
if (!repoFullName) {
|
if (!payloadName) {
|
||||||
return res.status(400).json({ error: 'repository.full_name fehlt' })
|
return res.status(400).json({ error: 'repository.full_name fehlt' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const branch = ref.replace('refs/heads/', '') || 'main'
|
const branch = ref.replace('refs/heads/', '') || 'main'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let previewConfigFile = await fetchPreviewConfig(repoFullName, branch)
|
// Phase 1: aktuellen Repo-Namen aufloesen - wartet auf einen laufenden Umzug DESSELBEN
|
||||||
|
// Repos (dieselbe Sperre wie moveRepoToOrg/importNewRepos, Controller-Ruling R17), damit
|
||||||
|
// der Webhook nie mit einem veralteten Namen weiterlaeuft. Keine Sperre bleibt darueber
|
||||||
|
// hinaus gehalten - ein Deploy kann lange dauern und darf sie nicht blockieren.
|
||||||
|
const name = await withLocks([repoLockKey(payloadName)], () => currentRepoFullName(payloadName), {
|
||||||
|
waitMs: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Phase 2: Preview-Config + Deploy wie bisher, mit `name` - ohne gehaltene Sperre.
|
||||||
|
let previewConfigFile = await fetchPreviewConfig(name, branch)
|
||||||
// Selbstheilung: Wurde .webklar-preview.json im Repo geloescht (z.B. beim
|
// Selbstheilung: Wurde .webklar-preview.json im Repo geloescht (z.B. beim
|
||||||
// Leerraeumen/Ueberarbeiten der Seite), aber das Projekt ist in Appwrite als
|
// Leerraeumen/Ueberarbeiten der Seite), aber das Projekt ist in Appwrite als
|
||||||
// gehostet bekannt, dann Config aus dem gespeicherten Stand wiederherstellen
|
// gehostet bekannt, dann Config aus dem gespeicherten Stand wiederherstellen
|
||||||
// (Datei ins Repo zurueckschreiben) und normal weiter-deployen.
|
// (Datei ins Repo zurueckschreiben) und normal weiter-deployen.
|
||||||
if (!previewConfigFile) {
|
if (!previewConfigFile) {
|
||||||
try {
|
try {
|
||||||
const restored = await ensureRepoPreviewConfig(repoFullName, branch)
|
const restored = await ensureRepoPreviewConfig(name, branch)
|
||||||
if (restored) previewConfigFile = restored
|
if (restored) previewConfigFile = restored
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[webhook] preview-config-Wiederherstellung fehlgeschlagen:', err.message)
|
console.error('[webhook] preview-config-Wiederherstellung fehlgeschlagen:', err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const previewConfig = previewConfigFile || defaultPreviewConfig(repoFullName)
|
const previewConfig = previewConfigFile || defaultPreviewConfig(name)
|
||||||
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
const hasPreview = Boolean(previewConfigFile?.enabled !== false && previewConfigFile)
|
||||||
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
// Nur Projekte mit aktivierter Preview bekommen eine Subdomain zugewiesen -
|
||||||
// Repo-only-Projekte (enabled:false, keine Subdomain) bleiben ohne Website.
|
// Repo-only-Projekte (enabled:false, keine Subdomain) bleiben ohne Website.
|
||||||
const subdomain = hasPreview ? previewConfig.subdomain || repoFullName.split('/').pop() : ''
|
const subdomain = hasPreview ? previewConfig.subdomain || name.split('/').pop() : ''
|
||||||
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
const previewUrl = subdomain ? buildPreviewUrl(subdomain) : ''
|
||||||
|
|
||||||
let deployResult = { deployed: false, skipped: true }
|
let deployResult = { deployed: false, skipped: true }
|
||||||
@@ -81,19 +92,28 @@ router.post('/gitea', async (req, res) => {
|
|||||||
|
|
||||||
if (hasPreview && previewConfig.enabled !== false && branch === configBranch) {
|
if (hasPreview && previewConfig.enabled !== false && branch === configBranch) {
|
||||||
try {
|
try {
|
||||||
deployResult = await runDeploy(repoFullName, branch, previewConfig)
|
deployResult = await runDeploy(name, branch, previewConfig)
|
||||||
} catch (deployErr) {
|
} catch (deployErr) {
|
||||||
console.error('[webhook] deploy failed:', deployErr.message)
|
console.error('[webhook] deploy failed:', deployErr.message)
|
||||||
deployResult = { deployed: false, error: deployErr.message }
|
deployResult = { deployed: false, error: deployErr.message }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 3: Dokument unter derselben Repo-Sperre wie Import/Umzug anlegen/aktualisieren.
|
||||||
|
// Namen dabei ERNEUT aufloesen - deckt einen Umzug ab, der waehrend des (unter Sperre
|
||||||
|
// laufenden) Deploys oben passiert ist. `current` ab hier fuer alle weiteren Schritte.
|
||||||
|
let current
|
||||||
|
const project = await withLocks(
|
||||||
|
[repoLockKey(name)],
|
||||||
|
async () => {
|
||||||
|
current = await currentRepoFullName(name)
|
||||||
|
|
||||||
const projectPayload = {
|
const projectPayload = {
|
||||||
projectName: previewConfig.displayName || repo.name || subdomain,
|
projectName: previewConfig.displayName || repo.name || subdomain,
|
||||||
templateName: previewConfig.templateName || config.gitea.templateRepo,
|
templateName: previewConfig.templateName || config.gitea.templateRepo,
|
||||||
giteaRepoUrl: repo.html_url || `${config.gitea.baseUrl}/${repoFullName}`,
|
giteaRepoUrl: repo.html_url || `${config.gitea.baseUrl}/${current}`,
|
||||||
giteaRepoName: repo.name || subdomain,
|
giteaRepoName: repo.name || subdomain,
|
||||||
repoFullName,
|
repoFullName: current,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subdomain) projectPayload.subdomain = subdomain
|
if (subdomain) projectPayload.subdomain = subdomain
|
||||||
@@ -109,35 +129,56 @@ router.post('/gitea', async (req, res) => {
|
|||||||
projectPayload.status = 'synced'
|
projectPayload.status = 'synced'
|
||||||
}
|
}
|
||||||
|
|
||||||
const project = await upsertWebsiteProjectByRepo(repoFullName, projectPayload)
|
return upsertWebsiteProjectByRepo(current, projectPayload)
|
||||||
invalidateProjectCache(repoFullName)
|
},
|
||||||
|
{ waitMs: 10000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Phase 4
|
||||||
|
invalidateProjectCache(current)
|
||||||
|
|
||||||
|
// Phase 5: nur bei einem echten Commit - Ticket + Arbeitsblatt unter der Projekt-Sperre
|
||||||
|
// (derselben wie assignProject/reconcileProject), damit ein gleichzeitiges Zuweisen nicht
|
||||||
|
// dieselbe ticketId-Aufloesung doppelt anstoesst.
|
||||||
let gitWorksheet = null
|
let gitWorksheet = null
|
||||||
if (commitSha && commitSha !== '0000000000000000000000000000000000000000') {
|
if (commitSha && commitSha !== '0000000000000000000000000000000000000000') {
|
||||||
|
gitWorksheet = await withLocks(
|
||||||
|
['project:' + project.$id],
|
||||||
|
async () => {
|
||||||
|
let fresh = project
|
||||||
|
try {
|
||||||
|
fresh = await getDocument(config.collections.websiteProjects, project.$id)
|
||||||
|
} catch {
|
||||||
|
// Nachlesen fehlgeschlagen: mit dem Upsert-Ergebnis von oben weitermachen (R17).
|
||||||
|
}
|
||||||
|
|
||||||
// Projekt ohne Ticket: automatisch ein Webpage-Ticket anlegen (nur mit Kunde)
|
// Projekt ohne Ticket: automatisch ein Webpage-Ticket anlegen (nur mit Kunde)
|
||||||
let ticketId = project.ticketId
|
let ticketId = fresh.ticketId
|
||||||
if (!ticketId) {
|
if (!ticketId) {
|
||||||
try {
|
try {
|
||||||
ticketId = await ensureProjectTicket(project, { repoFullName })
|
ticketId = await ensureProjectTicket(fresh, { repoFullName: current })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[webhook] Auto-Ticket fehlgeschlagen:', err.message)
|
console.error('[webhook] Auto-Ticket fehlgeschlagen:', err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ticketId) {
|
if (!ticketId) return null
|
||||||
gitWorksheet = await createGitPushWorksheet({
|
|
||||||
|
return createGitPushWorksheet({
|
||||||
ticketId,
|
ticketId,
|
||||||
repoFullName,
|
repoFullName: current,
|
||||||
branch,
|
branch,
|
||||||
commits,
|
commits,
|
||||||
pusher,
|
pusher,
|
||||||
commitSha,
|
commitSha,
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
{ waitMs: 10000 }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
repoFullName,
|
repoFullName: current,
|
||||||
subdomain,
|
subdomain,
|
||||||
previewUrl,
|
previewUrl,
|
||||||
deploy: deployResult,
|
deploy: deployResult,
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { test, before, after } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { createHmac } from 'node:crypto'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import express from 'express'
|
||||||
|
import { startFakeGitea } from './helpers/fakeGitea.js'
|
||||||
|
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
||||||
|
|
||||||
|
const WEBHOOK_TOKEN = 's3cret'
|
||||||
|
|
||||||
|
let gitea
|
||||||
|
let aw
|
||||||
|
let tmp
|
||||||
|
let originalCwd
|
||||||
|
let server
|
||||||
|
let baseUrl
|
||||||
|
|
||||||
|
let nextFeedId = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut einen Gitea-1.25-Feed-Eintrag vom Typ `commit_repo` nach (ein Commit) - derselbe Aufbau
|
||||||
|
* wie in test/projectAssignment.test.js (Task 6): `content` traegt das JSON
|
||||||
|
* `{Commits:[...], HeadCommit:{...}}` als String, wie im echten Aktivitaeten-Feed.
|
||||||
|
*/
|
||||||
|
function pushEntry({ sha, at, user, msg }) {
|
||||||
|
const login = user || 'JUSN'
|
||||||
|
const commit = {
|
||||||
|
Sha1: sha,
|
||||||
|
Message: msg || `Commit ${sha}`,
|
||||||
|
AuthorEmail: `${login.toLowerCase()}@example.com`,
|
||||||
|
AuthorName: login,
|
||||||
|
CommitterEmail: `${login.toLowerCase()}@example.com`,
|
||||||
|
CommitterName: login,
|
||||||
|
Timestamp: at,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: nextFeedId++,
|
||||||
|
op_type: 'commit_repo',
|
||||||
|
act_user: { login, full_name: '' },
|
||||||
|
ref_name: 'refs/heads/main',
|
||||||
|
created: at,
|
||||||
|
content: JSON.stringify({ Commits: [commit], HeadCommit: { Sha1: sha }, CompareURL: '', Len: 1 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wartet, bis die Fake-Gitea den POST .../transfer fuer `fullName` erhalten hat (Controller-
|
||||||
|
* Ruling R3), statt einen festen `sleep(50)` zu nutzen: `gitea.calls` waechst, SOBALD die
|
||||||
|
* Anfrage beim Fake-Server ankommt - strikt VOR einer etwaigen `setTransferDelay`-Verzoegerung
|
||||||
|
* der Antwort. So laesst sich der Webhook zuverlaessig genau dann abschicken, wenn der Umzug
|
||||||
|
* seine Repo-Sperren bereits haelt, ohne von der Terminzeit des Test-Rechners abzuhaengen.
|
||||||
|
*/
|
||||||
|
function waitForGiteaTransfer(fullName, timeoutMs = 1000) {
|
||||||
|
const transferPath = `/api/v1/repos/${fullName}/transfer`
|
||||||
|
const deadline = Date.now() + timeoutMs
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
;(function check() {
|
||||||
|
if (gitea.calls.some((c) => c.method === 'POST' && c.path === transferPath)) return resolve()
|
||||||
|
if (Date.now() > deadline) return reject(new Error(`Timeout beim Warten auf POST ${transferPath}`))
|
||||||
|
setTimeout(check, 5)
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(urlPath, body, jwt) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' }
|
||||||
|
if (jwt) headers.Authorization = `Bearer ${jwt}`
|
||||||
|
const res = await fetch(`${baseUrl}${urlPath}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body || {}),
|
||||||
|
})
|
||||||
|
const json = await res.json().catch(() => null)
|
||||||
|
return { status: res.status, body: json }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Baut einen Gitea-Push-Webhook-Payload (nur die vom Portal genutzten Felder). */
|
||||||
|
function pushPayload({ full_name, sha, pusher, branch = 'main' }) {
|
||||||
|
return {
|
||||||
|
ref: `refs/heads/${branch}`,
|
||||||
|
repository: {
|
||||||
|
full_name,
|
||||||
|
name: full_name.split('/').pop(),
|
||||||
|
html_url: `${gitea.baseUrl}/${full_name}`,
|
||||||
|
},
|
||||||
|
commits: [
|
||||||
|
{
|
||||||
|
id: sha,
|
||||||
|
message: `Commit ${sha}`,
|
||||||
|
author: { name: pusher },
|
||||||
|
url: `${gitea.baseUrl}/${full_name}/commit/${sha}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pusher: { username: pusher },
|
||||||
|
after: sha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function webhook(payload) {
|
||||||
|
const bodyText = JSON.stringify(payload)
|
||||||
|
const signature = createHmac('sha256', WEBHOOK_TOKEN).update(bodyText).digest('hex')
|
||||||
|
const res = await fetch(`${baseUrl}/webhook/gitea`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-Gitea-Signature': signature },
|
||||||
|
body: bodyText,
|
||||||
|
})
|
||||||
|
const json = await res.json().catch(() => null)
|
||||||
|
return { status: res.status, body: json }
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
gitea = await startFakeGitea({
|
||||||
|
repos: [{ full_name: 'JUSN/D-ner-Nation' }, { full_name: 'JUSN/langsam' }],
|
||||||
|
feeds: {
|
||||||
|
// Wie Task 6 (test/projectAssignment.test.js): genau ein nachtragbarer Push
|
||||||
|
// ('26bc22ec...' von JUSN), der Rest sind Alt-System-/Alt-Branch-Rauschen.
|
||||||
|
'JUSN/D-ner-Nation': [
|
||||||
|
pushEntry({ sha: 's1', user: 'webklar-system', at: '2026-09-27T14:50:00Z', msg: 'System-Push' }),
|
||||||
|
pushEntry({ sha: 'k1', user: 'knso', at: '2026-09-27T14:48:00Z', msg: 'Update preview config from ticket system' }),
|
||||||
|
pushEntry({ sha: '26bc22ecf17e2e5a5a8794dcb4cf3892f802c51d', user: 'JUSN', at: '2026-09-27T14:46:34Z', msg: 'Bilder aktualisiert' }),
|
||||||
|
{ ...pushEntry({ sha: 'x1', at: '2026-09-27T14:46:33Z' }), content: '' },
|
||||||
|
{ id: nextFeedId++, op_type: 'create_repo', created: '2026-09-27T14:02:46Z', act_user: { login: 'JUSN' } },
|
||||||
|
],
|
||||||
|
// p5/JUSN/langsam hat keine Pushes im Feed.
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
aw = await startFakeAppwrite({
|
||||||
|
seed: {
|
||||||
|
websiteProjects: [
|
||||||
|
{ $id: 'p1', repoFullName: 'JUSN/D-ner-Nation', projectName: 'D-ner-Nation', customerId: '', ticketId: '' },
|
||||||
|
{ $id: 'p5', repoFullName: 'JUSN/langsam', projectName: 'langsam', customerId: '', ticketId: '' },
|
||||||
|
],
|
||||||
|
workorders: [{ $id: 't1', woid: '100115' }],
|
||||||
|
customers: [{ $id: 'c1', name: 'Döner Nation' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
originalCwd = process.cwd()
|
||||||
|
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'gitea-routes-'))
|
||||||
|
process.chdir(tmp)
|
||||||
|
|
||||||
|
Object.assign(process.env, {
|
||||||
|
GITEA_BASE_URL: gitea.baseUrl,
|
||||||
|
GITEA_API_TOKEN: 'test-token',
|
||||||
|
GITEA_WEBHOOK_TOKEN: WEBHOOK_TOKEN,
|
||||||
|
APPWRITE_ENDPOINT: aw.endpoint,
|
||||||
|
APPWRITE_API_KEY: 'test-key',
|
||||||
|
PREVIEW_DEPLOY_ROOT: '',
|
||||||
|
TRAEFIK_DYNAMIC_DIR: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// config.js liest die Umgebung beim Import
|
||||||
|
const [{ default: adminGiteaRoutes }, { default: adminWebsiteProjectsRoutes }, { default: giteaWebhookRoutes }] =
|
||||||
|
await Promise.all([
|
||||||
|
import('../server/routes/admin/gitea.js'),
|
||||||
|
import('../server/routes/admin/websiteProjects.js'),
|
||||||
|
import('../server/routes/webhook/gitea.js'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
// rawBody wird fuer die HMAC-Verifikation des Gitea-Webhooks benoetigt - derselbe Aufbau wie server/index.js.
|
||||||
|
app.use(
|
||||||
|
express.json({
|
||||||
|
limit: '2mb',
|
||||||
|
verify: (req, _res, buf) => {
|
||||||
|
req.rawBody = buf
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
app.use('/api/admin/gitea', adminGiteaRoutes)
|
||||||
|
app.use('/api/admin/website-projects', adminWebsiteProjectsRoutes)
|
||||||
|
app.use('/webhook', giteaWebhookRoutes)
|
||||||
|
|
||||||
|
server = await new Promise((resolve) => {
|
||||||
|
const s = app.listen(0, '127.0.0.1', () => resolve(s))
|
||||||
|
})
|
||||||
|
baseUrl = `http://127.0.0.1:${server.address().port}`
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await new Promise((resolve) => server.close(resolve))
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
await gitea.close()
|
||||||
|
await aw.close()
|
||||||
|
await fs.rm(tmp, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('import-repos braucht Mitarbeiter-JWT', async () => {
|
||||||
|
assert.equal((await post('/api/admin/gitea/import-repos')).status, 401)
|
||||||
|
const r = await post('/api/admin/gitea/import-repos', {}, 'jwt-admin')
|
||||||
|
assert.equal(r.status, 200); assert.equal(typeof r.body.created, 'number')
|
||||||
|
})
|
||||||
|
test('assign über die Schnittstelle', async () => {
|
||||||
|
const r = await post('/api/admin/website-projects/p1/assign', { customerId: 'c1', ticketId: 't1' }, 'jwt-admin')
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
assert.deepEqual([r.body.moved.to, r.body.backfilled, r.body.warnings], ['WEBklar/D-ner-Nation', 1, []])
|
||||||
|
})
|
||||||
|
test('assign ohne Kunde und Ticket', async () => {
|
||||||
|
const r = await post('/api/admin/website-projects/p1/assign', {}, 'jwt-admin')
|
||||||
|
assert.deepEqual([r.status, r.body.error], [400, 'customerId oder ticketId erforderlich'])
|
||||||
|
})
|
||||||
|
test('pushes über die Schnittstelle', async () => {
|
||||||
|
const r = await post('/api/admin/website-projects/p1/pushes', {}, 'jwt-admin')
|
||||||
|
assert.deepEqual([r.status, r.body.moved, r.body.backfilled], [200, null, 0])
|
||||||
|
})
|
||||||
|
test('Webhook nach Umzug mit altem Namen', async () => {
|
||||||
|
const docs = aw.all('websiteProjects').length
|
||||||
|
const r = await webhook(pushPayload({ full_name: 'JUSN/D-ner-Nation', sha: 'f00dbeef', pusher: 'JUSN' }))
|
||||||
|
assert.equal(r.status, 200)
|
||||||
|
assert.equal(aw.all('websiteProjects').length, docs)
|
||||||
|
assert.ok(aw.all('worksheets').some((w) => w.workorderId === 't1' && w.details.includes('Commit: f00dbee')))
|
||||||
|
})
|
||||||
|
test('Webhook des System-Kontos', async () => {
|
||||||
|
const before = aw.all('worksheets').length
|
||||||
|
await webhook(pushPayload({ full_name: 'WEBklar/D-ner-Nation', sha: 'cafe0001', pusher: 'webklar-system' }))
|
||||||
|
assert.equal(aw.all('worksheets').length, before)
|
||||||
|
})
|
||||||
|
test('Webhook wartet auf laufenden Umzug', async () => {
|
||||||
|
gitea.setTransferDelay(300)
|
||||||
|
const move = post('/api/admin/website-projects/p5/assign', { customerId: 'c1', ticketId: 't1' }, 'jwt-admin') // p5: JUSN/langsam
|
||||||
|
await waitForGiteaTransfer('JUSN/langsam')
|
||||||
|
await webhook(pushPayload({ full_name: 'WEBklar/langsam', sha: 'abcd1234', pusher: 'JUSN' }))
|
||||||
|
await move
|
||||||
|
assert.equal(aw.all('websiteProjects').filter((d) => d.repoFullName === 'WEBklar/langsam').length, 1)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user