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>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-30 10:21:34 +00:00
1 parent 6e6c9a4c24
commit 405b2a0485
4 files changed
+277 -88

No files matched your search

+18 -1
View File
@@ -17,12 +17,17 @@ import { randomUUID } from 'node:crypto'
* all: (collection: string) => object[],
* get: (collection: string, id: string) => (object|undefined),
* calls: Array<{method: string, path: string, query: Record<string, string>, headers: Record<string, string>, body: any}>,
* failNext: (spec: {method: string, pathIncludes: string, status: number}) => void,
* close: () => Promise<void>,
* }>} `all`/`get` lesen den LIVE-Stand (nach create/patch/delete ueber HTTP). Die
* Datenbank-ID im Pfad wird nicht geprueft - jede wird akzeptiert.
* `failNext({method, pathIncludes, status})` laesst GENAU die naechste Anfrage, deren Methode
* passt und deren Pfad `pathIncludes` enthaelt, mit diesem Status und einer generischen
* JSON-Fehlermeldung scheitern (danach wieder normales Verhalten).
*/
export async function startFakeAppwrite(options = {}) {
const calls = []
const pendingFailures = []
/** @type {Map<string, Map<string, object>>} Sammlungsname -> (Dokument-ID -> Dokument). */
const store = new Map()
@@ -196,6 +201,14 @@ export async function startFakeAppwrite(options = {}) {
body: parsedBody,
})
const failIdx = pendingFailures.findIndex(
(f) => f.method === req.method && url.pathname.includes(f.pathIncludes)
)
if (failIdx !== -1) {
const [f] = pendingFailures.splice(failIdx, 1)
return send(res, f.status, { message: `fakeAppwrite: erzwungener Fehler (${f.status})` })
}
const route = ROUTES.find((r) => r.method === req.method && r.re.test(url.pathname))
if (!route) return send(res, 404, { message: 'Not Found' })
const match = route.re.exec(url.pathname)
@@ -228,10 +241,14 @@ export async function startFakeAppwrite(options = {}) {
}
}
function failNext({ method, pathIncludes, status }) {
pendingFailures.push({ method, pathIncludes, status })
}
function close() {
for (const socket of sockets) socket.destroy()
return new Promise((resolve) => server.close(() => resolve()))
}
return { endpoint, all, get, calls, close }
return { endpoint, all, get, calls, failNext, close }
}