Files
mesh-catalog/modules/gitea/test/commit-trees.test.ts
T

224 lines
11 KiB
TypeScript

import assert from "node:assert/strict";
import { test } from "node:test";
import { createServer } from "node:http";
// Fixtures follow Gitea's GitTreeResponse as its API documents it (swagger: sha, url, tree[{path, mode, type, size,
// sha, url}], truncated, page, total_count) and as the forge answers GET /repos/{o}/{r}/git/trees/{sha}; the entry
// fields are those, with sha values written out. The live capture (1234/AC6): GET
// /repos/novox/mesh-delivery/git/trees/7f61817 (non-recursive) answered the COMMIT's sha at the top, not the root
// tree's; the root tree is computed from the root's own entries, as git does.
import { createHash } from "node:crypto";
const COMMIT = "c".repeat(40);
const h = (c: string) => c.repeat(40);
const entry = (path: string, type: "tree" | "blob", sha: string) => ({
path, mode: type === "tree" ? "040000" : "100644", type, size: type === "tree" ? 0 : 12, sha, url: `u/${sha}`,
});
const LISTING = [
entry("README.md", "blob", h("1")),
entry("module.json", "blob", h("2")),
entry("modules", "tree", h("3")),
entry("modules/a", "tree", h("4")),
entry("modules/a/module.json", "blob", h("5")),
entry("modules/a/main.go", "blob", h("6")),
entry("modules/b", "tree", h("7")),
entry("modules/b/c", "tree", h("8")),
entry("modules/b/c/module.json", "blob", h("9")),
entry("modules/d", "tree", h("a")),
entry("modules/d/notes.md", "blob", h("b")),
];
/** git's own hash of a tree from its entries (an independent restatement for the fixtures): mode without leading zero. */
function gitTree(es: { path: string; type: string; sha: string; mode: string }[]): string {
const ordered = es.map((e) => ({ ...e, key: e.path + (e.type === "tree" ? "/" : "") })).sort((a, b) => Buffer.compare(Buffer.from(a.key), Buffer.from(b.key)));
const body = Buffer.concat(ordered.map((e) => Buffer.concat([Buffer.from(`${e.mode.replace(/^0+/, "")} ${e.path}\0`), Buffer.from(e.sha, "hex")])));
return createHash("sha1").update(Buffer.concat([Buffer.from(`tree ${body.length}\0`), body])).digest("hex");
}
const ROOT = gitTree(LISTING.filter((e) => !e.path.includes("/")));
async function serve(pages: any[]) {
const { GiteaClient } = await import("../client.ts");
const asked: string[] = [];
const server = createServer((req, res) => {
asked.push(req.url ?? "");
const url = new URL(req.url ?? "", "http://x");
if (url.pathname !== `/api/v1/repos/novox/hq/git/trees/${COMMIT}` || url.searchParams.get("recursive") !== "true") {
res.statusCode = 404;
return res.end("{}");
}
const page = Number(url.searchParams.get("page") ?? 1);
res.end(JSON.stringify(pages[page - 1] ?? { sha: COMMIT, tree: [], truncated: false, page, total_count: 0 }));
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
return { client, server, asked };
}
test("a commit's trees: the root and the tree of each directory holding a module.json", async () => {
// 1234/AC1 and 1234/AC4
const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
try {
const got = await client.commitTrees("novox", "hq", COMMIT);
assert.deepEqual(got, { root: ROOT, trees: { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") } });
} finally {
server.close();
}
});
test("a listing the forge pages is read page by page", async () => {
// 1234/AC1
const { client, server, asked } = await serve([
{ sha: COMMIT, tree: LISTING.slice(0, 5), truncated: true, page: 1, total_count: LISTING.length },
{ sha: COMMIT, tree: LISTING.slice(5, 9), truncated: true, page: 2, total_count: LISTING.length },
{ sha: COMMIT, tree: LISTING.slice(9), truncated: false, page: 3, total_count: LISTING.length },
]);
try {
const got = await client.commitTrees("novox", "hq", COMMIT);
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
assert.equal(asked.length, 3, `asked ${asked.join(" ")}`);
} finally {
server.close();
}
});
test("a listing the forge says is cut fails, naming the repository and the commit, and answers no trees", async () => {
// 1234/AC2
const { client, server } = await serve([{ sha: COMMIT, tree: LISTING, truncated: true, page: 1, total_count: LISTING.length }]);
try {
await assert.rejects(client.commitTrees("novox", "hq", COMMIT), (err: Error) => {
assert.match(err.message, /novox\/hq/);
assert.ok(err.message.includes(COMMIT), err.message);
assert.match(err.message, /cut/);
return true;
});
} finally {
server.close();
}
});
test("a sha that is not a full commit, or no owner or repo, is refused before the forge is asked", async () => {
// 1234/AC3
const { getGiteaTools } = await import("../tools/index.ts");
const { client, server, asked } = await serve([]);
try {
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
assert.ok(tool, "the tool is listed");
for (const args of [
{ owner: "novox", repo: "hq", sha: "abc1234" },
{ owner: "novox", repo: "hq", sha: "main" },
{ owner: "novox", repo: "hq", sha: "" },
{ owner: "novox", repo: "hq", sha: "c".repeat(41) },
{ owner: "", repo: "hq", sha: COMMIT },
{ owner: "novox", repo: " ", sha: COMMIT },
]) await assert.rejects(tool.run(args as any), /./, JSON.stringify(args));
assert.equal(asked.length, 0, `the forge was asked ${asked.join(" ")}`);
} finally {
server.close();
}
});
test("gitea_open_pulls says each pull request's head branch beside its head sha", async () => {
// 1234/AC5
const { GiteaClient } = await import("../client.ts");
const server = createServer((req, res) => {
const url = req.url ?? "";
const send = (v: unknown) => res.end(JSON.stringify(v));
if (url.startsWith("/api/v1/repos/issues/search")) {
return send(url.includes("page=1") ? [{ number: 7, repository: { owner: "novox", name: "hq", full_name: "novox/hq" } }] : []);
}
if (url.startsWith("/api/v1/repos/novox/hq/pulls/7")) {
return send({ number: 7, state: "open", base: { ref: "main" }, head: { ref: "feat/x", sha: "aaa1111" }, html_url: "h" });
}
if (url.includes("/branch_protections")) { res.statusCode = 403; return res.end("{}"); }
if (url.startsWith("/api/v1/repos/novox/hq/branches/main")) return send({ name: "main" });
if (url.includes("/commits/aaa1111/status")) return send({ statuses: [] });
res.statusCode = 404;
res.end("{}");
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
try {
const [p] = await client.openPulls();
assert.deepEqual(p, { owner: "novox", repo: "hq", number: 7, base: "main", head: "feat/x", head_sha: "aaa1111", html_url: "h",
required: [], statuses: [], checks: {}, labels: [] });
} finally {
server.close();
}
});
test("the root tree is git's hash of the root's own entries, never the listing's top sha (the commit's)", async () => {
// 1234/AC6, from the live capture named in the header: novox/mesh-delivery at 7f61817.
const commit = "7f618177d6773d8d2d2b9e0b10e13b85ec700c59";
const listing = [
entry("README.md", "blob", "a087d22cbcbecce42256315671ff7e4d6ca85a7c"),
entry("merge-check.sh", "blob", "c4ef9a3dd6390b0edde7e4137095553897cecfb0"),
entry("modules", "tree", "31c0abb312ee6ce4693a7aacdd8b3fba871d6cf3"),
entry("modules/x", "tree", h("d")),
entry("modules/x/module.json", "blob", h("e")),
];
const { GiteaClient } = await import("../client.ts");
const server = createServer((req, res) => {
res.end(JSON.stringify(new URL(req.url ?? "", "http://x").pathname.endsWith(`/git/trees/${commit}`)
? { sha: commit, tree: listing, truncated: false, page: 1, total_count: listing.length } : {}));
});
await new Promise<void>((r) => server.listen(0, r));
const client = new GiteaClient(`http://127.0.0.1:${(server.address() as any).port}`, "t");
try {
const got = await client.commitTrees("novox", "mesh-delivery", commit);
assert.equal(got.root, "659bcaaac6c16972ec380267a4eba69d15558c26");
assert.deepEqual(got.trees, { "modules/x": h("d") });
} finally {
server.close();
}
});
test("named paths: each asked path's git object id at the commit, a file's or a directory's, by directory (1758 S1, ADR 0499 §3)", async () => {
const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
try {
const got = await client.commitTrees("novox", "hq", COMMIT, { "modules/a": ["main.go", "module.json"], "modules/b": ["c"], "": ["README.md", "modules/d"] });
assert.deepEqual(got.paths, {
"modules/a": { "main.go": h("6"), "module.json": h("5") },
"modules/b": { c: h("8") },
"": { "README.md": h("1"), "modules/d": h("a") },
});
assert.deepEqual(got.trees, { "": ROOT, "modules/a": h("4"), "modules/b/c": h("8") });
assert.equal((await client.commitTrees("novox", "hq", COMMIT)).paths, undefined, "no paths asked, none answered");
} finally {
server.close();
}
});
test("a named path the listing lacks fails naming the directory and the path, and answers nothing", async () => {
const { client, server } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
try {
await assert.rejects(client.commitTrees("novox", "hq", COMMIT, { "modules/a": ["main.go", "gone.go"] }), (err: Error) => {
assert.match(err.message, /modules\/a/);
assert.match(err.message, /gone\.go/);
return true;
});
for (const bad of [[""], ["../x"], ["/abs"], ["a//b"]]) {
await assert.rejects(client.commitTrees("novox", "hq", COMMIT, { "modules/a": bad }), /./, JSON.stringify(bad));
}
} finally {
server.close();
}
});
test("gitea_commit_trees takes paths as {directory: [paths]} and refuses another shape before the forge is asked", async () => {
const { getGiteaTools } = await import("../tools/index.ts");
const { client, server, asked } = await serve([{ sha: COMMIT, url: "u", tree: LISTING, truncated: false, page: 1, total_count: LISTING.length }]);
try {
const tool = getGiteaTools(client).find((t) => t.name === "gitea_commit_trees")!;
assert.ok("paths" in tool.input, "the tool names the argument");
const got: any = await tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths: { "modules/a": ["main.go"] } } as any);
assert.deepEqual(got.paths, { "modules/a": { "main.go": h("6") } });
const before = asked.length;
for (const paths of ["modules/a", ["main.go"], { "modules/a": "main.go" }, { "modules/a": [1] }]) {
await assert.rejects(tool.run({ owner: "novox", repo: "hq", sha: COMMIT, paths } as any), /paths/, JSON.stringify(paths));
}
assert.equal(asked.length, before, "the forge was asked for a refused call");
} finally {
server.close();
}
});