import { HttpError } from "@batch-cooking/error-tools"; import { ErrorCode } from "@batch-cooking/shared"; import { expect } from "chai"; import { env } from "../src/config/env.js"; import { forgejo } from "../src/lib/forgejo/forgejo.service.js"; /** * Stubs `globalThis.fetch` for one test — same approach as * `recipe-sources/the-meal-db.test.ts` (no HTTP-mock library in this repo). * `capture` collects the `(url, init)` each call was made with, so a test * can assert the request shape. Restored by `afterEach`. */ function stubFetch( responder: (url: string, init: RequestInit | undefined) => Response | Promise, capture: { url: string; init: RequestInit | undefined }[], ) { globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { const url = typeof input === "string" ? input : input.toString(); capture.push({ url, init }); return responder(url, init); }) as typeof fetch; } const configured = forgejo.isConfigured(); describe("ForgejoService", () => { let originalFetch: typeof fetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; }); it("isConfigured() reports whether all three FORGEJO_* vars are set", () => { expect(forgejo.isConfigured()).to.equal( env.FORGEJO_BASE_URL !== undefined && env.FORGEJO_TOKEN !== undefined && env.FORGEJO_REPO !== undefined, ); }); describe("createIssue", () => { it("POSTs to the repo's issues endpoint with a `token` auth header and { title, body }", async function () { if (!configured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const calls: { url: string; init: RequestInit | undefined }[] = []; stubFetch( () => new Response(JSON.stringify({ number: 42, html_url: "https://forge/x/y/issues/42" }), { status: 201, }), calls, ); const result = await forgejo.createIssue({ title: "Un titre", body: "Un corps" }); expect(result).to.deep.equal({ number: 42, htmlUrl: "https://forge/x/y/issues/42" }); expect(calls).to.have.lengthOf(1); const [call] = calls; expect(call.url).to.equal(`${env.FORGEJO_BASE_URL}/api/v1/repos/${env.FORGEJO_REPO}/issues`); expect(call.init?.method).to.equal("POST"); const headers = call.init?.headers as Record; expect(headers.Authorization).to.equal(`token ${env.FORGEJO_TOKEN}`); expect(JSON.parse(String(call.init?.body))).to.deep.equal({ title: "Un titre", body: "Un corps", }); }); it("throws 502 FORGEJO_REQUEST_FAILED on a non-2xx response", async function () { if (!configured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } stubFetch(() => new Response("nope", { status: 403 }), []); try { await forgejo.createIssue({ title: "t", body: "b" }); expect.fail("expected createIssue to throw"); } catch (err) { expect(err).to.be.instanceOf(HttpError); expect((err as HttpError).status).to.equal(502); expect((err as HttpError).code).to.equal(ErrorCode.FORGEJO_REQUEST_FAILED); } }); it("throws 502 FORGEJO_REQUEST_FAILED on a network error", async function () { if (!configured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } stubFetch(() => { throw new Error("ECONNREFUSED"); }, []); try { await forgejo.createIssue({ title: "t", body: "b" }); expect.fail("expected createIssue to throw"); } catch (err) { expect(err).to.be.instanceOf(HttpError); expect((err as HttpError).code).to.equal(ErrorCode.FORGEJO_REQUEST_FAILED); } }); }); });