mesh/merge-gate out of date: judged against trunk 2edef5a00ae0, the trunk is now 0e7d9e74d22a; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 2edef5a00ae0, the trunk is now 0e7d9e74d22a; judged again on a recheck or when it is merged
mesh/delivery covered after its stop: a later walk that contains it was delivered: plan-1792011830935225963 (novox/mesh-catalog at 0361c44a into main, do…
The gitea module keeps one system webhook for every event family, made or corrected at start with the admin account (POST /admin/hooks, forge 1.27.3) and a secret it mints and keeps. Its receiver, at the docker bridge's gateway, refuses an unsigned request before reading it, a body over 5 MB, and a signature that does not verify; it answers a verified delivery at once, drops one already heard by its delivery id, and publishes each as forge.<kind>.<action> on the module's events (its own stream, ADR 0363, follows issue 725). A pull request opened, pushed to or merged is announced at once. The polls look every five minutes only while deliveries arrive, and every 30 s again, saying so loudly, when one finds what no event said or ten minutes pass without a delivery. The announced records keep both paths from announcing twice.
121 lines
6.6 KiB
TypeScript
121 lines
6.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { createHmac } from "node:crypto";
|
|
import type { AddressInfo } from "node:net";
|
|
|
|
// **The forge says every event on the bus** (novox/hq issue 729): a signed delivery is published once and acted on,
|
|
// an unsigned or unverified one is refused, and an event and the poll seeing the same head announce it once.
|
|
|
|
const secret = "s".repeat(64);
|
|
const sign = (body: string, key = secret) => createHmac("sha256", key).update(body).digest("hex");
|
|
const opened = JSON.stringify({ action: "opened", number: 7, pull_request: { number: 7, title: "t", user: { login: "jo" },
|
|
head: { ref: "feat/x", sha: "a".repeat(40) }, base: { ref: "main" }, merged: false },
|
|
repository: { name: "hq", full_name: "novox/hq", owner: { login: "novox" } }, sender: { login: "jo" } });
|
|
|
|
async function serve(published: { name: string }[], acted: { name: string }[]) {
|
|
const { receiver, HOOK_PATH } = await import("../webhook.ts");
|
|
const server = receiver({ secret, port: 0, log: () => {},
|
|
publish: async (e) => { published.push(e); }, act: async (e) => { acted.push(e); } });
|
|
await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}${HOOK_PATH}`;
|
|
return { server, url };
|
|
}
|
|
|
|
test("a signed event is published on the bus and acted on once", async () => {
|
|
const published: { name: string }[] = [], acted: { name: string }[] = [];
|
|
const { server, url } = await serve(published, acted);
|
|
try {
|
|
const headers = { "X-Gitea-Event": "pull_request", "X-Gitea-Signature": sign(opened), "X-Gitea-Delivery": "d-1" };
|
|
const res = await fetch(url, { method: "POST", body: opened, headers });
|
|
assert.equal(res.status, 200);
|
|
// The same delivery sent again is answered and published nowhere.
|
|
const again = await fetch(url, { method: "POST", body: opened, headers });
|
|
assert.equal(again.status, 200);
|
|
await server.idle();
|
|
assert.deepEqual(published.map((e) => e.name), ["forge.pull-request.opened"]);
|
|
assert.deepEqual(acted.map((e) => e.name), ["forge.pull-request.opened"]);
|
|
} finally { server.close(); }
|
|
});
|
|
|
|
test("an unsigned event, or one whose signature does not verify, is refused and published nowhere", async () => {
|
|
const published: { name: string }[] = [], acted: { name: string }[] = [];
|
|
const { server, url } = await serve(published, acted);
|
|
try {
|
|
const unsigned = await fetch(url, { method: "POST", body: opened, headers: { "X-Gitea-Event": "pull_request" } });
|
|
assert.equal(unsigned.status, 401);
|
|
assert.match(await unsigned.text(), /unsigned/);
|
|
const forged = await fetch(url, { method: "POST", body: opened,
|
|
headers: { "X-Gitea-Event": "pull_request", "X-Gitea-Signature": sign(opened, "another secret") } });
|
|
assert.equal(forged.status, 401);
|
|
assert.match(await forged.text(), /does not verify/);
|
|
const big = "x".repeat(5 * 1024 * 1024 + 1);
|
|
const large = await fetch(url, { method: "POST", body: big,
|
|
headers: { "X-Gitea-Event": "push", "X-Gitea-Signature": sign(big) } });
|
|
assert.equal(large.status, 413);
|
|
await server.idle();
|
|
assert.deepEqual(published, []);
|
|
assert.deepEqual(acted, []);
|
|
} finally { server.close(); }
|
|
});
|
|
|
|
test("an event and the poll seeing the same head announce it once", async () => {
|
|
const { claimHead } = await import("../webhook.ts");
|
|
const heads: Record<string, string> = {};
|
|
const sha = "a".repeat(40);
|
|
assert.equal(claimHead(heads, "novox/hq#7", sha), true, "the event claims the head and announces it");
|
|
assert.equal(claimHead(heads, "novox/hq#7", sha), false, "the poll finds it taken");
|
|
assert.equal(claimHead(heads, "novox/hq#7", "b".repeat(40)), true, "a new head is announced again");
|
|
});
|
|
|
|
test("each forge event is named in the module's stream and trimmed", async () => {
|
|
const { eventOf } = await import("../webhook.ts");
|
|
const merged = eventOf("pull_request", { action: "closed", pull_request: { number: 3, merged: true, merge_commit_sha: "c".repeat(40),
|
|
head: { sha: "a".repeat(40) }, base: { ref: "main" } }, repository: { name: "hq", owner: { login: "novox" } } });
|
|
assert.equal(merged.name, "forge.pull-request.merged");
|
|
assert.equal(merged.body.merge_commit_sha, "c".repeat(40));
|
|
assert.equal(eventOf("pull_request", { action: "label_updated", pull_request: { number: 3 }, label: { name: "mesh/ready" } }).name,
|
|
"forge.pull-request.labeled");
|
|
const comment = eventOf("issue_comment", { action: "created", is_pull: true, issue: { number: 3 },
|
|
comment: { id: 9, body: "ready at abc", user: { login: "jo" } } });
|
|
assert.equal(comment.name, "forge.pull-request.commented");
|
|
assert.equal(comment.body.comment, "ready at abc");
|
|
assert.equal(eventOf("push", { ref: "refs/heads/main", after: "d".repeat(40) }).name, "forge.push");
|
|
assert.equal(eventOf("pull_request_review_approved", {}).name, "forge.other");
|
|
});
|
|
|
|
test("the system webhook is made when none points at the receiver, and corrected when one does", async () => {
|
|
const { ensureSystemHook } = await import("../webhook.ts");
|
|
const calls: string[] = [];
|
|
let hooks: unknown[] = [];
|
|
const admin = { call: async (method: string, path: string, body?: any) => {
|
|
calls.push(`${method} ${path}`);
|
|
if (method === "GET") return { status: 200, body: hooks };
|
|
if (method === "POST") { assert.ok(body.events.includes("pull_request") && body.events.includes("push")); assert.equal(body.config.secret, secret); return { status: 201, body: {} }; }
|
|
return { status: 200, body: {} };
|
|
} };
|
|
assert.equal(await ensureSystemHook(admin, "http://h:3047/forge-event", secret), "created");
|
|
hooks = [{ id: 4, config: { url: "http://h:3047/forge-event" } }];
|
|
assert.equal(await ensureSystemHook(admin, "http://h:3047/forge-event", secret), "corrected");
|
|
assert.ok(calls.includes("PATCH /admin/hooks/4"));
|
|
});
|
|
|
|
test("the polls look every 30 s until events are heard, every five minutes while they are, and 30 s again on a miss", async () => {
|
|
const { Pace } = await import("../webhook.ts");
|
|
let now = 1_000_000;
|
|
const said: string[] = [];
|
|
const pace = new Pace(() => now, (l) => said.push(l));
|
|
assert.equal(pace.every(), 30_000, "no delivery yet");
|
|
pace.heard();
|
|
assert.equal(pace.every(), 300_000, "a delivery heard");
|
|
now += 11 * 60_000;
|
|
assert.equal(pace.every(), 30_000, "ten minutes without a delivery");
|
|
pace.heard();
|
|
now += 1000;
|
|
pace.miss("novox/hq#7 at aaaaaaaa");
|
|
assert.equal(pace.every(), 30_000, "the poll found what no event said");
|
|
assert.match(said.join("\n"), /MISSED novox\/hq#7/);
|
|
now += 1000;
|
|
pace.heard();
|
|
assert.equal(pace.every(), 300_000, "the next delivery");
|
|
});
|