Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166import { describe, expect, it } from "vitest";import { GitlabError, addNote, createIssue, findIssueByKey, findOpenIssueByMarker, uploadFile, type GitlabTarget } from "../../report/gitlab";
const TOKEN = "glpat-SECRETSECRETSECRET1234"; // aite:allow-secret (a made-up sentinel for a test)const target: GitlabTarget = { url: "http://localhost:8929", project: "root/aite-verdict-tests", token: TOKEN };
interface Call { method: string; url: string; headers: Record<string, string>; body?: unknown }/** A GitLab that answers from a function and records every call. */function fakeGitlab(answer: (c: Call) => { status: number; json?: unknown; text?: string } | Error) { const calls: Call[] = []; const fetchImpl = (async (url: string, init?: RequestInit) => { const headers = Object.fromEntries(Object.entries((init?.headers ?? {}) as Record<string, string>)); const call: Call = { method: init?.method ?? "GET", url: String(url), headers, body: typeof init?.body === "string" ? JSON.parse(init.body) : init?.body }; calls.push(call); const a = answer(call); if (a instanceof Error) throw a; return { ok: a.status >= 200 && a.status < 300, status: a.status, json: async () => { if (a.text !== undefined) throw new SyntaxError("not json"); return a.json; }, text: async () => a.text ?? JSON.stringify(a.json) } as Response; }) as unknown as typeof fetch; return { calls, deps: { fetch: fetchImpl } };}
describe("createIssue", () => { it("posts the title and description to the project, with the token in the header and only there", async () => { const g = fakeGitlab(() => ({ status: 201, json: { iid: 7, web_url: "http://localhost:8929/root/aite-verdict-tests/-/issues/7" } })); const issue = await createIssue(target, { title: "t", description: "d" }, g.deps); expect(issue).toEqual({ iid: 7, url: "http://localhost:8929/root/aite-verdict-tests/-/issues/7", reference: "root/aite-verdict-tests#7" }); expect(g.calls).toHaveLength(1); const c = g.calls[0]!; expect(c.method).toBe("POST"); expect(c.url).toBe("http://localhost:8929/api/v4/projects/root%2Faite-verdict-tests/issues"); expect(c.headers["PRIVATE-TOKEN"]).toBe(TOKEN); expect(c.url).not.toContain(TOKEN); expect(JSON.stringify(c.body)).not.toContain(TOKEN); expect(c.body).toEqual({ title: "t", description: "d" }); });
it("P4-3.e: 401, 403, 404, 500, a timeout and a dead network are each an error of their own kind, after ONE attempt, and never mention the token", async () => { const cases: Array<[string, () => { status: number; json?: unknown } | Error, string]> = [ ["401", () => ({ status: 401, json: { message: `401 Unauthorized ${TOKEN}` } }), "auth"], ["403", () => ({ status: 403, json: { message: "403 Forbidden" } }), "forbidden"], ["404", () => ({ status: 404, json: { message: "404 Project Not Found" } }), "not-found"], ["500", () => ({ status: 500, json: { message: "boom" } }), "server"], ["503", () => ({ status: 503, json: {} }), "server"], ["timeout", () => Object.assign(new Error("aborted"), { name: "AbortError" }), "timeout"], ["network", () => new TypeError(`fetch failed: ECONNREFUSED to ${TOKEN}`), "network"], ]; for (const [name, answer, kind] of cases) { const g = fakeGitlab(answer); const err = await createIssue(target, { title: "t", description: "d" }, g.deps).then(() => undefined, (e: unknown) => e as GitlabError); expect(err, name).toBeInstanceOf(GitlabError); expect(err!.kind, name).toBe(kind); expect(err!.message, name).not.toContain(TOKEN); expect(g.calls, `${name}: no retry`).toHaveLength(1); } });
it("an answer that is not what GitLab says is a bad-response, not a crash", async () => { const noJson = fakeGitlab(() => ({ status: 201, text: "<html>proxy</html>" })); await expect(createIssue(target, { title: "t", description: "d" }, noJson.deps)).rejects.toMatchObject({ kind: "bad-response" }); const noIid = fakeGitlab(() => ({ status: 201, json: { nothing: true } })); await expect(createIssue(target, { title: "t", description: "d" }, noIid.deps)).rejects.toMatchObject({ kind: "bad-response" }); });
it("refuses a url that is not http or https, and a project path with anything odd in it", async () => { const g = fakeGitlab(() => ({ status: 201, json: { iid: 1, web_url: "x" } })); await expect(createIssue({ ...target, url: "file:///etc" }, { title: "t", description: "d" }, g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(createIssue({ ...target, project: "../../admin" }, { title: "t", description: "d" }, g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(createIssue({ ...target, project: "" }, { title: "t", description: "d" }, g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(createIssue({ ...target, token: "" }, { title: "t", description: "d" }, g.deps)).rejects.toMatchObject({ kind: "config" }); expect(g.calls).toHaveLength(0); });});
describe("findIssueByKey (P4-3.g: one issue however many clicks)", () => { it("asks GitLab for issues with the key in the description, open or closed, and returns the first that really has it", async () => { const key = "0123456789abcdef"; const g = fakeGitlab(() => ({ status: 200, json: [{ iid: 3, web_url: "u3", description: "unrelated" }, { iid: 4, web_url: "u4", description: `text\n<!-- aite-verdict-key: ${key} -->` }] })); const found = await findIssueByKey(target, key, g.deps); expect(found).toEqual({ iid: 4, url: "u4", reference: "root/aite-verdict-tests#4" }); expect(g.calls[0]!.url).toBe(`http://localhost:8929/api/v4/projects/root%2Faite-verdict-tests/issues?search=${key}&in=description&state=all&per_page=20`); }); it("is undefined when none has it", async () => { const g = fakeGitlab(() => ({ status: 200, json: [{ iid: 3, web_url: "u3", description: "nothing" }] })); expect(await findIssueByKey(target, "0123456789abcdef", g.deps)).toBeUndefined(); }); it("refuses a key that is not 16 hex digits, so nothing odd goes into the query", async () => { const g = fakeGitlab(() => ({ status: 200, json: [] })); await expect(findIssueByKey(target, "a&b=c", g.deps)).rejects.toMatchObject({ kind: "config" }); expect(g.calls).toHaveLength(0); }); it("its errors are the same kinds", async () => { const g = fakeGitlab(() => ({ status: 401, json: {} })); await expect(findIssueByKey(target, "0123456789abcdef", g.deps)).rejects.toMatchObject({ kind: "auth" }); });});
describe("uploadFile", () => { it("uploads the bytes and returns the markdown GitLab gives back", async () => { const g = fakeGitlab(() => ({ status: 201, json: { markdown: "", url: "/uploads/abc/002-01.png" } })); const md = await uploadFile(target, "002-01.png", Buffer.from("png bytes"), g.deps); expect(md).toBe(""); expect(g.calls[0]!.url).toBe("http://localhost:8929/api/v4/projects/root%2Faite-verdict-tests/uploads"); expect(g.calls[0]!.method).toBe("POST"); expect(g.calls[0]!.headers["PRIVATE-TOKEN"]).toBe(TOKEN); }); it("only a plain png name is accepted, and an answer with no markdown is a bad-response", async () => { const g = fakeGitlab(() => ({ status: 201, json: {} })); await expect(uploadFile(target, "../x.png", Buffer.from("x"), g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(uploadFile(target, "ok.png", Buffer.from("x"), g.deps)).rejects.toMatchObject({ kind: "bad-response" }); }); it("uploads a run report (.html) and accepts the plain link GitLab gives for a file that is not a picture", async () => { const g = fakeGitlab(() => ({ status: 201, json: { markdown: "[report.html](/uploads/abc/report.html)" } })); expect(await uploadFile(target, "report.html", Buffer.from("<html>"), g.deps)).toBe("[report.html](/uploads/abc/report.html)"); await expect(uploadFile(target, "report.exe", Buffer.from("x"), g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(uploadFile(target, ".html", Buffer.from("x"), g.deps)).rejects.toMatchObject({ kind: "config" }); }); it("a markdown answer that is not an image of ours is refused (nothing from GitLab is trusted blindly)", async () => { const g = fakeGitlab(() => ({ status: 201, json: { markdown: "[x](javascript:alert(1))" } })); await expect(uploadFile(target, "ok.png", Buffer.from("x"), g.deps)).rejects.toMatchObject({ kind: "bad-response" }); });});
describe("findOpenIssueByMarker and addNote (P7-3)", () => { const KEY = "0123456789abcdef"; const MARKER = `<!-- aite-script-key: ${KEY} -->`; const issue = (iid: number, description: string) => ({ iid, description, web_url: `http://localhost:8929/root/aite-verdict-tests/-/issues/${iid}` });
it("asks only for open issues and returns the one whose description carries the exact marker", async () => { const g = fakeGitlab(() => ({ status: 200, json: [issue(3, `mentions ${KEY} but has no marker`), issue(4, `text\n${MARKER}`)] })); const hit = await findOpenIssueByMarker(target, "aite-script-key", KEY, g.deps); expect(hit).toEqual({ iid: 4, url: "http://localhost:8929/root/aite-verdict-tests/-/issues/4", reference: "root/aite-verdict-tests#4" }); expect(g.calls[0]!.url).toContain(`search=${KEY}`); expect(g.calls[0]!.url).toContain("state=opened"); expect(g.calls[0]!.method).toBe("GET"); }); it("finds nothing when no open issue has the marker, and refuses a key or marker name of the wrong shape before any call", async () => { const g = fakeGitlab(() => ({ status: 200, json: [issue(3, "other")] })); expect(await findOpenIssueByMarker(target, "aite-script-key", KEY, g.deps)).toBeUndefined(); const bad = fakeGitlab(() => ({ status: 200, json: [] })); await expect(findOpenIssueByMarker(target, "aite-script-key", "nope", bad.deps)).rejects.toMatchObject({ kind: "config" }); await expect(findOpenIssueByMarker(target, "bad name!", KEY, bad.deps)).rejects.toMatchObject({ kind: "config" }); expect(bad.calls).toHaveLength(0); }); it("a list that is not a list is a bad response", async () => { const g = fakeGitlab(() => ({ status: 200, json: { not: "a list" } })); await expect(findOpenIssueByMarker(target, "aite-script-key", KEY, g.deps)).rejects.toMatchObject({ kind: "bad-response" }); }); it("addNote posts the body to that issue's notes, with the token only in the header", async () => { const g = fakeGitlab(() => ({ status: 201, json: { id: 9 } })); await addNote(target, 4, "still failing", g.deps); const c = g.calls[0]!; expect(c.method).toBe("POST"); expect(c.url).toBe("http://localhost:8929/api/v4/projects/root%2Faite-verdict-tests/issues/4/notes"); expect(c.body).toEqual({ body: "still failing" }); expect(c.headers["PRIVATE-TOKEN"]).toBe(TOKEN); expect(JSON.stringify(c.body)).not.toContain(TOKEN); }); it("addNote refuses an issue number that is not a positive whole number, and reports an error from GitLab once", async () => { const g = fakeGitlab(() => ({ status: 404, json: { message: "404 Not found" } })); await expect(addNote(target, 0, "x", g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(addNote(target, 1.5, "x", g.deps)).rejects.toMatchObject({ kind: "config" }); await expect(addNote(target, 4, "x", g.deps)).rejects.toBeInstanceOf(GitlabError); expect(g.calls).toHaveLength(1); });});