Files
mesh-catalog/modules/gitea/test/create-repo.test.ts
T

118 lines
6.0 KiB
TypeScript

// A repository is created where it is asked for (novox/hq issue 1124): gitea_create_repo created under the tools'
// own user alone, so a repository of the novox organisation could not be made through it. With `owner` it is created
// in that organisation (POST /orgs/{org}/repos), the forge's refusal said with its status and its reason; without
// it, under the tools' own user as before.
import { test, after, beforeEach } from "node:test";
import assert from "node:assert/strict";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { GiteaClient } from "../dist/client.js";
import { ConfiguredToken } from "../dist/token.js";
import { getGiteaTools } from "../dist/tools/index.js";
interface Sent { method: string; path: string; body: any }
const state = {
sent: [] as Sent[],
// When set, what the forge answers a create in an organisation: its status and its message.
refuse: null as { status: number; message: string } | null,
};
function json(res: ServerResponse, status: number, body: unknown) {
res.statusCode = status;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(body));
}
const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
const raw = Buffer.concat(chunks).toString();
const path = new URL(req.url ?? "", "http://x").pathname.replace(/^\/api\/v1/, "");
const method = req.method ?? "GET";
state.sent.push({ method, path, body: raw ? JSON.parse(raw) : undefined });
const created = (owner: string) => {
const body = raw ? JSON.parse(raw) : {};
return json(res, 201, { full_name: `${owner}/${body.name}`, name: body.name, owner: { login: owner },
private: body.private, description: body.description, html_url: `http://forge/${owner}/${body.name}`,
clone_url: `http://forge/${owner}/${body.name}.git`, default_branch: "main" });
};
if (method === "POST" && path === "/user/repos") return created("agent");
const org = /^\/orgs\/([^/]+)\/repos$/.exec(path);
if (method === "POST" && org) {
if (state.refuse) return json(res, state.refuse.status, { message: state.refuse.message, url: "http://forge/api/swagger" });
return created(decodeURIComponent(org[1]));
}
return json(res, 404, { message: "not found" });
});
await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
after(() => new Promise<void>((r) => server.close(() => r())));
const { port } = server.address() as { port: number };
const client = new GiteaClient(`http://127.0.0.1:${port}`, new ConfiguredToken("t"));
const tool = (name: string) => getGiteaTools(client).find((t) => t.name === name)!;
const body = { name: "mesh-delivery", description: "the delivery's own repository", private: true, auto_init: true };
beforeEach(() => {
state.sent = [];
state.refuse = null;
});
test("with an organisation the repository is created there, with the same body, and answered (1124/AC1)", async () => {
// Fails before the fix: createRepo takes no owner and posts to /user/repos, so the repository is the tools' user's.
const repo = await client.createRepo({ ...body }, "novox");
assert.deepEqual(state.sent, [{ method: "POST", path: "/orgs/novox/repos", body }]);
assert.equal(repo.full_name, "novox/mesh-delivery");
assert.equal(repo.owner, "novox");
// The tool passes its owner through; before the fix it has no owner argument and posts to /user/repos.
state.sent = [];
const answered = await tool("gitea_create_repo").run({ owner: "novox", ...body }) as { repo: { full_name: string } };
assert.deepEqual(state.sent, [{ method: "POST", path: "/orgs/novox/repos", body }]);
assert.equal(answered.repo.full_name, "novox/mesh-delivery");
assert.match(tool("gitea_create_repo").description, /organisation/, "the description says where it creates");
});
test("without an owner the repository is created under the tools' own user, as before (1124/AC2)", async () => {
// Holds before the fix: it is the behaviour the fix must leave unchanged.
const repo = await client.createRepo({ ...body });
assert.deepEqual(state.sent, [{ method: "POST", path: "/user/repos", body }]);
assert.equal(repo.full_name, "agent/mesh-delivery");
state.sent = [];
await tool("gitea_create_repo").run({ name: "mesh-delivery" });
assert.deepEqual(state.sent, [{ method: "POST", path: "/user/repos", body: { name: "mesh-delivery", private: true, auto_init: true } }]);
});
test("the forge's refusal to create in an organisation is said with its status, its reason and the organisation (1124/AC3)", async () => {
// Fails before the fix: the create goes to /user/repos, which the fake forge answers 201, so nothing is refused.
for (const [status, message] of [
[403, "user should be an owner or a member of the organisation"],
[404, "GetOrgByName: org does not exist [name: novox]"],
[409, "The repository with the same name already exists."],
] as const) {
state.refuse = { status, message };
for (const run of [
() => client.createRepo({ ...body }, "novox"),
() => tool("gitea_create_repo").run({ owner: "novox", ...body }),
]) {
await assert.rejects(run(), (e: Error) => {
assert.match(e.message, new RegExp(`\\b${status}\\b`), `the status ${status} is said`);
assert.ok(e.message.includes(message), `the forge's reason is said: ${e.message}`);
assert.match(e.message, /organisation novox/, "the organisation is named");
return true;
});
}
}
});
test("an owner that is not a name is refused before anything is sent (1124/AC4)", async () => {
// Holds before the fix too: namesChecked (tools/index.ts) checks args.owner for every tool. It stays so the guard
// keeps covering this tool's new argument.
for (const owner of ["..", "novox/../agent", "", "no vox", "%2e%2e", "-x"]) {
await assert.rejects(tool("gitea_create_repo").run({ owner, name: "mesh-delivery" }), /not a repository owner/,
JSON.stringify(owner));
}
assert.deepEqual(state.sent, [], "nothing reached the forge");
});