Ein kopiertes Repo (git clone + push, Vorlage) bringt die .webklar-preview.json des Originals mit. Sein erster Push ueberschrieb die Preview des Originals, danach scheiterte der Webhook mit 500 (Unique-Index auf subdomain). Fast passiert bei Olzinger -> HSK-Vorlage, 04.10.2026. - foreignSubdomainOwner: Subdomain gehoert einem anderen Repo oder einem Projekt ohne Repo; ein in Gitea umgezogenes Repo (Weiterleitung) zaehlt nicht als fremd - Webhook: fremde Subdomain -> kein Deploy, Projekt ohne Preview (neu: synced, Repo-Name statt fremdem Anzeigenamen); eine schon vergebene eigene Preview bleibt, WOMS zeigt den Grund als Preview-Fehler - runDeploy prueft fuer alle anderen Wege (WOMS, Reaktivieren), vor dem Runner, damit ein gesperrter Aufruf keinen laufenden Bau verwaist; Appwrite-Fehler bei der Pruefung blockieren dort nicht (Webhook streng) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
250 lines
11 KiB
JavaScript
250 lines
11 KiB
JavaScript
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'
|
|
|
|
// Sperre gegen fremde Subdomains: Ein kopiertes Repo (git clone + push, Vorlage) bringt die
|
|
// .webklar-preview.json des Originals mit. Ohne Sperre ueberschrieb sein erster Push die
|
|
// Preview des Originals (Olzinger -> HSK-Vorlage, 04.10.2026).
|
|
|
|
const WEBHOOK_TOKEN = 's3cret'
|
|
const ORIGINAL = 'WEBklar/Olzinger'
|
|
const KOPIE = 'WEBklar/HSK-Kopie'
|
|
|
|
const previewJson = (subdomain) =>
|
|
JSON.stringify({ enabled: true, type: 'static', branch: 'main', displayName: 'Olzinger', subdomain })
|
|
|
|
let gitea
|
|
let aw
|
|
let tmp
|
|
let originalCwd
|
|
let server
|
|
let baseUrl
|
|
let foreignSubdomainOwner
|
|
let runDeploy
|
|
let foreignSubdomainBlock
|
|
|
|
function pushPayload(repo, { sha, branch = 'main', pusher = 'knso' }) {
|
|
return {
|
|
ref: `refs/heads/${branch}`,
|
|
repository: { full_name: repo, name: repo.split('/')[1], html_url: `${gitea.baseUrl}/${repo}` },
|
|
commits: [{ id: sha, message: `Commit ${sha}`, author: { name: pusher } }],
|
|
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,
|
|
})
|
|
return { status: res.status, body: await res.json().catch(() => null) }
|
|
}
|
|
|
|
const docOf = (repo) => aw.all('websiteProjects').find((d) => d.repoFullName === repo)
|
|
|
|
before(async () => {
|
|
gitea = await startFakeGitea({
|
|
repos: [
|
|
{ full_name: ORIGINAL },
|
|
{ full_name: KOPIE },
|
|
{ full_name: 'WEBklar/neu-name' },
|
|
{ full_name: 'WEBklar/relaunch-kopie' },
|
|
{ full_name: 'WEBklar/eigene-seite' },
|
|
],
|
|
redirects: { 'knso/alt-name': 'WEBklar/neu-name' },
|
|
contents: {
|
|
[`${ORIGINAL}:.webklar-preview.json`]: previewJson('olzinger'),
|
|
[`${KOPIE}:.webklar-preview.json`]: previewJson('olzinger'),
|
|
'WEBklar/relaunch-kopie:.webklar-preview.json': previewJson('relaunch'),
|
|
// hat schon eine eigene Preview, Config von Hand auf eine fremde Subdomain gestellt
|
|
'WEBklar/eigene-seite:.webklar-preview.json': previewJson('olzinger'),
|
|
},
|
|
})
|
|
aw = await startFakeAppwrite({
|
|
unique: { websiteProjects: ['repoFullName', 'subdomain'] },
|
|
seed: {
|
|
websiteProjects: [
|
|
{ $id: 'olz', repoFullName: ORIGINAL, projectName: 'Olzinger', subdomain: 'olzinger', customerId: 'c1', ticketId: 't1', status: 'deployed' },
|
|
// per Gitea umgezogen (alter Name leitet weiter), Dokument noch unter dem alten Namen
|
|
{ $id: 'umzug', repoFullName: 'knso/alt-name', projectName: 'Umzug', subdomain: 'umzug-seite' },
|
|
// Repo in Gitea geloescht, Dokument haelt die Subdomain noch
|
|
{ $id: 'weg', repoFullName: 'knso/geloescht', projectName: 'Weg', subdomain: 'weg-seite' },
|
|
// gehostet ohne Repo (wie die Relaunch-Vorschau)
|
|
{ $id: 'rel', repoFullName: '', projectName: 'WEBklar Relaunch', subdomain: 'relaunch' },
|
|
{ $id: 'eigen', repoFullName: 'WEBklar/eigene-seite', projectName: 'Eigene Seite', subdomain: 'eigene-seite', status: 'deployed', provisioningStatus: 'ready' },
|
|
],
|
|
workorders: [{ $id: 't1', woid: '100130', customerName: 'Olzinger', topic: 'Website', status: 'Open' }],
|
|
customers: [{ $id: 'c1', name: 'Olzinger GmbH' }],
|
|
employees: [{ $id: 'k', displayName: 'Kenso Grimm', shortcode: 'KNSO', email: 'kenso@webklar.com' }],
|
|
},
|
|
})
|
|
|
|
originalCwd = process.cwd()
|
|
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'subdomain-sperre-'))
|
|
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: '',
|
|
PUSH_TIME_SECRET: 'w'.repeat(40),
|
|
})
|
|
|
|
const [{ default: giteaWebhookRoutes }, previewDeploy, mailer] = await Promise.all([
|
|
import('../server/routes/webhook/gitea.js'),
|
|
import('../server/services/previewDeploy.js'),
|
|
import('../server/services/mailer.js'),
|
|
])
|
|
foreignSubdomainOwner = previewDeploy.foreignSubdomainOwner
|
|
runDeploy = previewDeploy.runDeploy
|
|
foreignSubdomainBlock = previewDeploy.foreignSubdomainBlock
|
|
mailer.setMailTransportForTests({ sendMail: async () => ({}) })
|
|
|
|
const app = express()
|
|
app.use(express.json({ limit: '2mb', verify: (req, _res, buf) => { req.rawBody = buf } }))
|
|
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 () => {
|
|
if (server) await new Promise((resolve) => server.close(resolve))
|
|
if (originalCwd) process.chdir(originalCwd)
|
|
await gitea?.close()
|
|
await aw?.close()
|
|
if (tmp) await fs.rm(tmp, { recursive: true, force: true })
|
|
})
|
|
|
|
test('foreignSubdomainOwner: freie Subdomain und eigenes Repo sind nicht fremd', async () => {
|
|
assert.equal(await foreignSubdomainOwner(KOPIE, 'noch-frei'), '')
|
|
assert.equal(await foreignSubdomainOwner(ORIGINAL, 'olzinger'), '')
|
|
assert.equal(await foreignSubdomainOwner('webklar/OLZINGER', 'olzinger'), '', 'Schreibweise egal')
|
|
assert.equal(await foreignSubdomainOwner(KOPIE, ''), '')
|
|
})
|
|
|
|
test('foreignSubdomainOwner: Subdomain eines anderen Repos ist fremd', async () => {
|
|
assert.equal(await foreignSubdomainOwner(KOPIE, 'olzinger'), ORIGINAL)
|
|
})
|
|
|
|
test('foreignSubdomainOwner: in Gitea umgezogenes Repo ist nicht fremd, geloeschtes schon', async () => {
|
|
assert.equal(await foreignSubdomainOwner('WEBklar/neu-name', 'umzug-seite'), '')
|
|
assert.equal(await foreignSubdomainOwner(KOPIE, 'weg-seite'), 'knso/geloescht')
|
|
})
|
|
|
|
test('foreignSubdomainOwner: Projekt ohne Repo haelt seine Subdomain, sperrt sich aber nicht selbst', async () => {
|
|
assert.equal(await foreignSubdomainOwner(KOPIE, 'relaunch'), 'Projekt „WEBklar Relaunch“ (ohne Repo)')
|
|
assert.equal(await foreignSubdomainOwner('', 'relaunch'), '')
|
|
})
|
|
|
|
test('runDeploy verweigert eine fremde Subdomain, bevor irgendetwas geklont wird', async () => {
|
|
const before = gitea.calls.length
|
|
const r = await runDeploy(KOPIE, 'main', { enabled: true, type: 'static', subdomain: 'olzinger' })
|
|
assert.equal(r.deployed, false)
|
|
assert.equal(r.blocked, true)
|
|
assert.equal(r.reason, 'subdomain_foreign')
|
|
assert.equal(r.subdomain, 'olzinger')
|
|
assert.equal(r.owner, ORIGINAL)
|
|
// nur die Besitzer-Pruefung fragt Gitea (nach dem Original), fuer die Kopie wird nichts geholt
|
|
assert.deepEqual(gitea.calls.slice(before).map((c) => `${c.method} ${c.path}`), [`GET /api/v1/repos/${ORIGINAL}`])
|
|
})
|
|
|
|
test('runDeploy: Pruefung nicht moeglich (Appwrite-Fehler) → Deploy laeuft trotzdem', async () => {
|
|
aw.failNext({ method: 'GET', pathIncludes: 'websiteProjects', status: 500 })
|
|
const r = await runDeploy(KOPIE, 'main', { enabled: true, type: 'static', subdomain: 'olzinger' })
|
|
assert.equal(r.blocked, undefined)
|
|
assert.equal(r.reason, 'PREVIEW_DEPLOY_ROOT nicht gesetzt')
|
|
})
|
|
|
|
test('foreignSubdomainBlock ohne failOpen (Webhook): Appwrite-Fehler wird weitergereicht', async () => {
|
|
aw.failNext({ method: 'GET', pathIncludes: 'websiteProjects', status: 500 })
|
|
await assert.rejects(foreignSubdomainBlock(KOPIE, 'olzinger'))
|
|
assert.equal((await foreignSubdomainBlock(KOPIE, 'olzinger')).owner, ORIGINAL)
|
|
})
|
|
|
|
test('runDeploy: eigene Subdomain des uebergebenen Projekts und subdomainChecked brauchen keine Abfrage', async () => {
|
|
const calls = aw.calls.length
|
|
const own = await runDeploy(ORIGINAL, 'main', { subdomain: 'olzinger' }, { project: aw.get('websiteProjects', 'olz') })
|
|
assert.equal(own.reason, 'PREVIEW_DEPLOY_ROOT nicht gesetzt')
|
|
const checked = await runDeploy(KOPIE, 'main', { subdomain: 'olzinger' }, { subdomainChecked: true })
|
|
assert.equal(checked.blocked, undefined, 'der Aufrufer hat schon geprueft')
|
|
assert.equal(aw.calls.length, calls)
|
|
})
|
|
|
|
test('runDeploy: eigene Subdomain laeuft weiter wie bisher', async () => {
|
|
const r = await runDeploy(ORIGINAL, 'main', { enabled: true, type: 'static', subdomain: 'olzinger' })
|
|
assert.equal(r.blocked, undefined)
|
|
assert.equal(r.reason, 'PREVIEW_DEPLOY_ROOT nicht gesetzt')
|
|
})
|
|
|
|
test('Webhook: Push einer Kopie mit fremder Subdomain → kein Deploy, Projekt ohne Preview', async () => {
|
|
const r = await webhook(pushPayload(KOPIE, { sha: 'aaaa1111' }))
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.subdomain, '')
|
|
assert.equal(r.body.previewUrl, '')
|
|
assert.equal(r.body.deploy.blocked, true)
|
|
assert.equal(r.body.deploy.owner, ORIGINAL)
|
|
|
|
const kopie = docOf(KOPIE)
|
|
assert.ok(kopie, 'Dokument der Kopie angelegt')
|
|
assert.ok(!kopie.subdomain, 'Kopie bekommt die fremde Subdomain nicht')
|
|
assert.equal(kopie.status, 'synced')
|
|
assert.equal(kopie.projectName, 'HSK-Kopie', 'nicht der Anzeigename aus der fremden Config')
|
|
|
|
const original = aw.get('websiteProjects', 'olz')
|
|
assert.equal(original.subdomain, 'olzinger')
|
|
assert.equal(original.status, 'deployed')
|
|
})
|
|
|
|
test('Webhook: Kopie auf anderem Branch bekommt die fremde Subdomain auch nicht', async () => {
|
|
const r = await webhook(pushPayload(KOPIE, { sha: 'bbbb2222', branch: 'entwurf' }))
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.subdomain, '')
|
|
assert.ok(!docOf(KOPIE).subdomain)
|
|
})
|
|
|
|
test('Webhook: Kopie mit der Subdomain eines Projekts ohne Repo wird gesperrt', async () => {
|
|
const r = await webhook(pushPayload('WEBklar/relaunch-kopie', { sha: 'cccc3333' }))
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.deploy.blocked, true)
|
|
assert.equal(aw.get('websiteProjects', 'rel').subdomain, 'relaunch')
|
|
})
|
|
|
|
test('Webhook: Push des Originals deployt wie bisher', async () => {
|
|
const r = await webhook(pushPayload(ORIGINAL, { sha: 'dddd4444' }))
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.subdomain, 'olzinger')
|
|
assert.equal(r.body.deploy.blocked, undefined)
|
|
assert.equal(r.body.deploy.reason, 'PREVIEW_DEPLOY_ROOT nicht gesetzt')
|
|
assert.equal(docOf(ORIGINAL).subdomain, 'olzinger')
|
|
})
|
|
|
|
test('Webhook: Projekt mit eigener Preview und fremder Subdomain in der Config bleibt, wie es ist', async () => {
|
|
const r = await webhook(pushPayload('WEBklar/eigene-seite', { sha: 'ffff6666' }))
|
|
assert.equal(r.status, 200)
|
|
assert.equal(r.body.deploy.blocked, true)
|
|
const doc = aw.get('websiteProjects', 'eigen')
|
|
assert.equal(doc.subdomain, 'eigene-seite')
|
|
assert.equal(doc.status, 'deployed')
|
|
assert.equal(doc.provisioningStatus, 'ready')
|
|
assert.equal(doc.projectName, 'Eigene Seite')
|
|
assert.equal(doc.previewState, 'failed', 'WOMS zeigt den Fehlertext nur bei failed')
|
|
assert.match(doc.previewError, /„olzinger“, die gehört WEBklar\/Olzinger/)
|
|
assert.equal(aw.get('websiteProjects', 'olz').subdomain, 'olzinger')
|
|
})
|