CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
api.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| fc1817a | 1 | import { describe, it, expect, beforeAll, afterAll } from "bun:test"; |
| 2 | import { join } from "path"; | |
| 3 | import { rm, mkdir } from "fs/promises"; | |
| 4 | import app from "../app"; | |
| 5 | ||
| 6 | const TEST_REPOS = join(import.meta.dir, "../../.test-repos-api"); | |
| 7 | ||
| 8 | beforeAll(async () => { | |
| 9 | process.env.GIT_REPOS_PATH = TEST_REPOS; | |
| 10 | process.env.DATABASE_URL = process.env.DATABASE_URL || ""; | |
| 11 | await mkdir(TEST_REPOS, { recursive: true }); | |
| 12 | }); | |
| 13 | ||
| 14 | afterAll(async () => { | |
| 15 | await rm(TEST_REPOS, { recursive: true, force: true }); | |
| 16 | }); | |
| 17 | ||
| 18 | describe("API routes", () => { | |
| 19 | it("GET / returns home page", async () => { | |
| 20 | const res = await app.request("/"); | |
| 21 | expect(res.status).toBe(200); | |
| 22 | const text = await res.text(); | |
| 23 | expect(text).toContain("gluecron"); | |
| 24 | }); | |
| 25 | ||
| 63807e5 | 26 | it("GET /api/repos/:owner/:name returns 404 or 503, never 500", async () => { |
| fc1817a | 27 | const res = await app.request("/api/repos/nobody/nothing"); |
| 63807e5 | 28 | // Without DB: 503 (db unreachable). With DB + missing repo: 404. |
| 29 | // API must degrade gracefully — 500 is NOT acceptable. | |
| 30 | expect([404, 503]).toContain(res.status); | |
| fc1817a | 31 | }); |
| 32 | ||
| 63807e5 | 33 | it("POST /api/repos returns 400 without required fields, never 500", async () => { |
| fc1817a | 34 | const res = await app.request("/api/repos", { |
| 35 | method: "POST", | |
| 36 | headers: { "Content-Type": "application/json" }, | |
| 37 | body: JSON.stringify({}), | |
| 38 | }); | |
| 63807e5 | 39 | // Validation happens before DB access — always 400. |
| 40 | expect(res.status).toBe(400); | |
| 41 | }); | |
| 42 | ||
| 43 | it("GET /api/users/:u/repos returns 404 or 503, never 500", async () => { | |
| 44 | const res = await app.request("/api/users/nobody/repos"); | |
| 45 | expect([404, 503]).toContain(res.status); | |
| fc1817a | 46 | }); |
| 47 | }); | |
| 48 | ||
| 49 | describe("Git HTTP routes", () => { | |
| 50 | it("returns 400 for invalid service", async () => { | |
| 51 | const res = await app.request("/test/repo.git/info/refs?service=invalid"); | |
| 52 | expect(res.status).toBe(400); | |
| 53 | }); | |
| 54 | ||
| 55 | it("returns 404 for non-existent repo", async () => { | |
| 56 | const res = await app.request( | |
| 57 | "/nobody/nothing.git/info/refs?service=git-upload-pack" | |
| 58 | ); | |
| 59 | expect(res.status).toBe(404); | |
| 60 | }); | |
| 61 | }); | |
| a2b53b6 | 62 | |
| 63 | describe("Git HTTP param stripping (Hono :repo.git regression)", () => { | |
| 64 | // The Hono upgrade changed what `:repo.git` captures: the param now | |
| 65 | // arrives as "repo.git" → "<name>.git" (suffix included). Every handler | |
| 66 | // must resolve names through gitParams, which strips the suffix — passing | |
| 67 | // the raw segment to repoExists/getRepoPath yields "<name>.git.git" on | |
| 68 | // disk and 404s every clone/push platform-wide. | |
| 69 | const { gitParams } = require("../routes/git").__test; | |
| 70 | const fakeCtx = (params: Record<string, string>) => | |
| 71 | ({ req: { param: () => params } }) as any; | |
| 72 | ||
| 73 | it("strips .git from the modern 'repo.git' param key", () => { | |
| 74 | expect(gitParams(fakeCtx({ owner: "a", "repo.git": "proj.git" }))).toEqual({ | |
| 75 | owner: "a", | |
| 76 | repo: "proj", | |
| 77 | }); | |
| 78 | }); | |
| 79 | ||
| 80 | it("handles dotted repo names (only the final .git is stripped)", () => { | |
| 81 | expect( | |
| 82 | gitParams(fakeCtx({ owner: "ccantynz", "repo.git": "Gluecron.com.git" })) | |
| 83 | ).toEqual({ owner: "ccantynz", repo: "Gluecron.com" }); | |
| 84 | }); | |
| 85 | ||
| 86 | it("falls back to the legacy 'repo' param key", () => { | |
| 87 | expect(gitParams(fakeCtx({ owner: "a", repo: "proj.git" }))).toEqual({ | |
| 88 | owner: "a", | |
| 89 | repo: "proj", | |
| 90 | }); | |
| 91 | }); | |
| 92 | ||
| 93 | it("pins live Hono param behavior for the :repo.git pattern", async () => { | |
| 94 | // If a future Hono bump changes param naming again, this fails loudly | |
| 95 | // instead of silently 404ing production clones. | |
| 96 | const { Hono } = await import("hono"); | |
| 97 | const probe = new Hono(); | |
| 98 | let seen: Record<string, string> = {}; | |
| 99 | probe.get("/:owner/:repo.git/info/refs", (c) => { | |
| 100 | seen = c.req.param() as Record<string, string>; | |
| 101 | return c.text("ok"); | |
| 102 | }); | |
| 103 | await probe.request("/alice/My.Repo.git/info/refs"); | |
| 104 | expect(gitParams({ req: { param: () => seen } } as any)).toEqual({ | |
| 105 | owner: "alice", | |
| 106 | repo: "My.Repo", | |
| 107 | }); | |
| 108 | }); | |
| 109 | }); |