diff --git a/src/context.ts b/src/context.ts index 1049b78..120d487 100644 --- a/src/context.ts +++ b/src/context.ts @@ -60,6 +60,17 @@ function hostnameOf(url: string, origin: string): string { } } +/** + * Normalize a Gitea base URL to the host root the client expects. The client + * (gitea-js) appends `/api/v1` itself, so a value that already carries it — a + * natural guess when the variable is literally named `..._API_URL` — would double + * the segment and 404 as a spurious `REPO_NOT_FOUND`. Strip a trailing `/api/v1` + * (with any trailing slashes) so the host base and the API endpoint both work. + */ +function normalizeApiBase(url: string): string { + return url.replace(/\/+$/, "").replace(/\/api\/v1$/, ""); +} + function resolveTestModeContext( deps: CliDeps, apiUrl: string, @@ -72,10 +83,11 @@ function resolveTestModeContext( ["Set `GITEA_AXI_REPO=OWNER/NAME` or pass `-R OWNER/NAME`"], ); } + const base = normalizeApiBase(apiUrl); return { ...parseRepoSpec(overrides.repoSpec, overrides.repoOrigin), - host: hostnameOf(apiUrl, "`GITEA_AXI_API_URL`"), - apiUrl: apiUrl.replace(/\/+$/, ""), + host: hostnameOf(base, "`GITEA_AXI_API_URL`"), + apiUrl: base, token: deps.env.GITEA_AXI_TOKEN ?? "", repoSource: overrides.repoSource, loginSource: overrides.loginSource, @@ -184,7 +196,7 @@ export async function resolveRepoContext(deps: CliDeps): Promise { owner, name, host, - apiUrl: login.url.replace(/\/+$/, ""), + apiUrl: normalizeApiBase(login.url), token, repoSource: overrides.repoSource, loginSource: overrides.loginSource, diff --git a/test/context.test.ts b/test/context.test.ts index 1e2ec3f..55282c0 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -1,4 +1,6 @@ import { afterEach, describe, expect, it } from "vitest"; +import { resolveRepoContext } from "../src/context.js"; +import type { CliDeps } from "../src/deps.js"; import { startFixtureServer, type FixtureServer } from "./fixture-server.js"; import { runCliTest, testModeEnv } from "./harness.js"; @@ -117,3 +119,49 @@ describe("context overrides", () => { expect(exitCode).toBe(0); }); }); + +describe("apiUrl normalization", () => { + function depsWithApiUrl(apiUrl: string): CliDeps { + return { + env: { + GITEA_AXI_API_URL: apiUrl, + GITEA_AXI_REPO: "acme/widgets", + GITEA_AXI_TOKEN: "test-token", + }, + cwd: process.cwd(), + globals: {}, + }; + } + + it("strips a trailing /api/v1 suffix from the host base", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/api/v1"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("strips a trailing /api/v1/ with a trailing slash", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/api/v1/"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("leaves a host base without an /api/v1 suffix unchanged", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); + + it("strips a lone trailing slash from the host base", async () => { + const context = await resolveRepoContext( + depsWithApiUrl("https://git.example.com/"), + ); + + expect(context.apiUrl).toBe("https://git.example.com"); + }); +});