Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
@@ -29,6 +29,12 @@ interface Forge {
|
||||
mints: number;
|
||||
lastScopes: string[] | null;
|
||||
tokens: Map<string, string>;
|
||||
/** Tokens beside `tokens` that carry a name already held there: what two mints at once leave behind. */
|
||||
twins: { id: number; name: string; sha1: string }[];
|
||||
/** How each DELETE named its token: "id" or "name". */
|
||||
deletedBy: string[];
|
||||
/** Plant a token by name, beside any other of the same name, as a raced mint would. */
|
||||
plant(name: string): void;
|
||||
scopesOf: Map<string, string[]>;
|
||||
admins: Map<string, string>;
|
||||
pullState: string;
|
||||
@@ -39,9 +45,14 @@ interface Forge {
|
||||
|
||||
function fakeForge(): Promise<Forge> {
|
||||
const forge = {
|
||||
mintedFor: "",
|
||||
mints: 0,
|
||||
lastScopes: null as string[] | null,
|
||||
tokens: new Map<string, string>(), // name -> value
|
||||
twins: [] as { id: number; name: string; sha1: string }[],
|
||||
deletedBy: [] as string[],
|
||||
ids: new Map<string, number>(), // value -> id, as gitea numbers its tokens
|
||||
nextId: 0,
|
||||
scopesOf: new Map<string, string[]>(), // value -> scopes, so a route can enforce them like gitea does
|
||||
admins: new Map([[ADMIN, PASSWORD]]),
|
||||
pullState: "open",
|
||||
@@ -73,7 +84,14 @@ function fakeForge(): Promise<Forge> {
|
||||
const tokens = url.pathname.match(/^\/api\/v1\/users\/([^/]+)\/tokens(?:\/([^/]+))?$/);
|
||||
if (tokens) {
|
||||
const user = basic(req);
|
||||
if (user === null || user !== decodeURIComponent(tokens[1])) return json(res, 401, { message: "auth required" });
|
||||
// gitea's reqSelfOrAdmin: the account itself, or the administrator for another (the tools' account, ADR 0365).
|
||||
if (user === null || (user !== decodeURIComponent(tokens[1]) && user !== ADMIN)) return json(res, 401, { message: "auth required" });
|
||||
// This fake keeps one account's tokens: the tools' account's when the module mints for it (ADR 0365), and the
|
||||
// admin's own list is then empty — nothing of it is retired.
|
||||
if (decodeURIComponent(tokens[1]) === ADMIN && forge.mintedFor === "mesh-tools") {
|
||||
return req.method === "GET" ? json(res, 200, []) : json(res, 404, { message: "token not found" });
|
||||
}
|
||||
if (req.method === "POST") forge.mintedFor = decodeURIComponent(tokens[1]);
|
||||
if (req.method === "POST") {
|
||||
const { name, scopes } = await body(req);
|
||||
if (forge.tokens.has(name)) return json(res, 400, { message: "token name has already been used" });
|
||||
@@ -82,12 +100,35 @@ function fakeForge(): Promise<Forge> {
|
||||
const sha1 = `minted-${forge.mints}-${Math.random().toString(36).slice(2)}`;
|
||||
forge.tokens.set(name, sha1);
|
||||
forge.scopesOf.set(sha1, scopes);
|
||||
return json(res, 201, { id: forge.mints, name, sha1, scopes, token_last_eight: sha1.slice(-8) });
|
||||
forge.ids.set(sha1, ++forge.nextId);
|
||||
return json(res, 201, { id: forge.ids.get(sha1), name, sha1, scopes, token_last_eight: sha1.slice(-8) });
|
||||
}
|
||||
// gitea's list: every token of the account, a page at a time (at most three to a page here, so a
|
||||
// client that reads only the first page misses some), the values never shown.
|
||||
const listed = (): { id: number; name: string; sha1: string }[] => [
|
||||
...[...forge.tokens].map(([name, sha1]) => ({ id: forge.ids.get(sha1) ?? 0, name, sha1 })),
|
||||
...forge.twins,
|
||||
];
|
||||
if (req.method === "GET" && !tokens[2]) {
|
||||
const page = Math.max(1, Number(url.searchParams.get("page") ?? "1"));
|
||||
const limit = Math.min(3, Math.max(1, Number(url.searchParams.get("limit") ?? "3")));
|
||||
const all = listed();
|
||||
res.setHeader("X-Total-Count", String(all.length));
|
||||
return json(res, 200, all.slice((page - 1) * limit, page * limit)
|
||||
.map((t) => ({ id: t.id, name: t.name, sha1: "", token_last_eight: t.sha1.slice(-8), scopes: [] })));
|
||||
}
|
||||
if (req.method === "DELETE" && tokens[2]) {
|
||||
const name = decodeURIComponent(tokens[2]);
|
||||
if (!forge.tokens.has(name)) return json(res, 404, { message: "token not found" });
|
||||
forge.tokens.delete(name);
|
||||
// gitea: "identified by ID and if not available by name" — and a name held twice is refused.
|
||||
const key = decodeURIComponent(tokens[2]);
|
||||
const byId = /^\d+$/.test(key);
|
||||
const matches = listed().filter((t) => (byId ? String(t.id) === key : t.name === key));
|
||||
if (matches.length === 0) return json(res, 404, { message: "token not found" });
|
||||
if (matches.length > 1) return json(res, 422, { message: `multiple matches for token name '${key}'` });
|
||||
const t = matches[0];
|
||||
forge.deletedBy.push(byId ? "id" : "name");
|
||||
const twin = forge.twins.findIndex((w) => w.id === t.id);
|
||||
if (twin >= 0) forge.twins.splice(twin, 1);
|
||||
else forge.tokens.delete(t.name);
|
||||
return json(res, 204, null);
|
||||
}
|
||||
return json(res, 405, { message: "method not allowed" });
|
||||
@@ -153,6 +194,17 @@ function fakeForge(): Promise<Forge> {
|
||||
{ full_name: "novox/hq", name: "hq", owner: { login: "novox" }, private: true, html_url: "http://fake/novox/hq" },
|
||||
]);
|
||||
}
|
||||
// What setting up the tools' account asks with the admin login (tools-account.ts, ADR 0365): answered plainly.
|
||||
if (basic(req) === ADMIN) {
|
||||
const p = url.pathname;
|
||||
if (req.method === "GET" && p === "/api/v1/user") return json(res, 200, { login: ADMIN, is_admin: true });
|
||||
if (req.method === "GET" && p === "/api/v1/users/mesh-tools") return json(res, 200, { login: "mesh-tools", is_admin: false });
|
||||
if (req.method === "POST" && p === "/api/v1/admin/users") return json(res, 422, { message: "user already exists" });
|
||||
if (req.method === "PATCH" && p === "/api/v1/admin/users/mesh-tools") return json(res, 200, { login: "mesh-tools" });
|
||||
if (req.method === "GET" && p === "/api/v1/orgs/novox/teams") return json(res, 200, [{ id: 7, name: "mesh-tools" }]);
|
||||
if (req.method === "PATCH" && p === "/api/v1/teams/7") return json(res, 200, { id: 7 });
|
||||
if (req.method === "PUT" && p === "/api/v1/teams/7/members/mesh-tools") return json(res, 204, null);
|
||||
}
|
||||
const adminUser = url.pathname.match(/^\/api\/v1\/admin\/users\/([^/]+)$/);
|
||||
if (adminUser && req.method === "PATCH") {
|
||||
const h = req.headers.authorization ?? "";
|
||||
@@ -181,6 +233,14 @@ function fakeForge(): Promise<Forge> {
|
||||
get pullTitle() { return forge.pullTitle; },
|
||||
get branchDeleted() { return forge.branchDeleted; },
|
||||
tokens: forge.tokens,
|
||||
twins: forge.twins,
|
||||
deletedBy: forge.deletedBy,
|
||||
plant: (name: string) => {
|
||||
const sha1 = `planted-${++forge.nextId}`;
|
||||
forge.ids.set(sha1, forge.nextId);
|
||||
if (forge.tokens.has(name)) forge.twins.push({ id: forge.nextId, name, sha1 });
|
||||
else forge.tokens.set(name, sha1);
|
||||
},
|
||||
scopesOf: forge.scopesOf,
|
||||
admins: forge.admins,
|
||||
close: () => new Promise((r) => server.close(() => r())),
|
||||
@@ -335,6 +395,56 @@ test("concurrent first calls share one mint", async () => {
|
||||
assert.equal(forge.mints, before + 1);
|
||||
});
|
||||
|
||||
test("two tokens hold the name (two mints raced): both are deleted by their id, and one fresh token is minted", async () => {
|
||||
const { env, file, logs } = await delivered(forge);
|
||||
forge.tokens.clear();
|
||||
forge.twins.length = 0;
|
||||
for (const other of ["laptop", "backup", "ci"]) forge.plant(other); // the account's other tokens: kept
|
||||
forge.plant("mesh-tools");
|
||||
forge.plant("mesh-tools"); // gitea refuses to delete either by name from here on
|
||||
forge.deletedBy.length = 0;
|
||||
const before = forge.mints;
|
||||
|
||||
const repos = await minted(env, logs).listRepos();
|
||||
|
||||
assert.equal(repos.length, 1);
|
||||
assert.equal(forge.mints, before + 1);
|
||||
assert.deepEqual(forge.deletedBy, ["id", "id"]);
|
||||
assert.equal(forge.twins.length, 0);
|
||||
assert.deepEqual([...forge.tokens.keys()].sort(), ["backup", "ci", "laptop", "mesh-tools"]);
|
||||
assert.equal(await readFile(file, "utf8"), forge.tokens.get("mesh-tools") + "\n");
|
||||
assert.ok(logs.some((l) => l.includes('holds 2 tokens named "mesh-tools"')), logs.join("\n"));
|
||||
});
|
||||
|
||||
test("two processes on one kept file, both without a token, mint once: the second waits for the first and reuses it", async () => {
|
||||
const { env, logs } = await delivered(forge);
|
||||
forge.tokens.clear();
|
||||
forge.twins.length = 0;
|
||||
const before = forge.mints;
|
||||
// Two sources over one state directory, as the tools bundle and the watcher are two processes.
|
||||
const first = minted(env, logs);
|
||||
const second = minted(env, logs);
|
||||
|
||||
await Promise.all([first.listRepos(), second.listRepos()]);
|
||||
|
||||
assert.equal(forge.mints, before + 1);
|
||||
assert.equal(forge.twins.length, 0);
|
||||
assert.ok(logs.some((l) => l.includes("another process minted it")), logs.join("\n"));
|
||||
});
|
||||
|
||||
test("a lock left by a process that died mid-mint is taken over, not waited on for ever", async () => {
|
||||
const { env, file, logs } = await delivered(forge);
|
||||
forge.tokens.clear();
|
||||
forge.twins.length = 0;
|
||||
await mkdir(join(env.MESH_GITEA_STATE_DIR!), { recursive: true });
|
||||
await writeFile(`${file}.lock`, `2147483646 ${Date.now()} gone\n`); // a pid no process holds
|
||||
const before = forge.mints;
|
||||
|
||||
assert.equal((await minted(env, logs).listRepos()).length, 1);
|
||||
assert.equal(forge.mints, before + 1);
|
||||
await assert.rejects(stat(`${file}.lock`), /ENOENT/);
|
||||
});
|
||||
|
||||
test("the admin account is refused: said plainly, nothing kept, and the next call fails the same way rather than crashing", async () => {
|
||||
const { env, file, logs } = await delivered(forge);
|
||||
forge.admins.delete(ADMIN); // the forge's data came from a predecessor; mesh-admin was never created there
|
||||
|
||||
Reference in New Issue
Block a user