Public: anyone can read this; the crew can change it.

The API's origin repository list names its Server-Timing phases: session, origin, render #3931

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3923-the-api-s-origin-repository-list-names-i → main 4 commits fork

  • Merged. scout/3923-the-api-s-origin-repository-list-names-i was merged into main as d3a6be0.

Review

Sign in to review this change.
2 files changed +94 −6
src/http/api.ts +11 −6
@@ -4421,19 +4421,24 @@ export function apiRoutes(router: Router<Handler>): void {
44214421 });
44224422
44234423 router.add("GET", "/api/v1/user/origins/:id/repos", async (ctx) => {
4424 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
44244425 const denied = requireAccount(ctx, { adminToken: true });
44254426 if (denied) return denied;
4426 const listed = await listRemoteRepos(
4427 ctx.app,
4428 ctx.auth.user,
4429 ctx.params.id!,
4430 ctx.url.searchParams.get("refresh") === "1",
4427 const listed = await timePhaseAsync(ctx.request, "origin", () =>
4428 listRemoteRepos(
4429 ctx.app,
4430 ctx.auth.user,
4431 ctx.params.id!,
4432 ctx.url.searchParams.get("refresh") === "1",
4433 ),
44314434 );
44324435 if (!listed.ok)
44334436 return listed.error === "unreachable"
44344437 ? apiError("origin_unreachable", 502, "the origin could not be reached")
44354438 : respondAccess(listed.error, 404);
4436 return json(listed.value.map((row) => remoteRepoJson(ctx, row)));
4439 return timePhase(ctx.request, "render", () =>
4440 json(listed.value.map((row) => remoteRepoJson(ctx, row))),
4441 );
44374442 });
44384443
44394444 // ── browsing an origin, live ─────────────────────────────────────────
test/api-origin-repos-timing.test.ts added +83
@@ -0,0 +1,83 @@
1 /** The API's origin repository list names its Server-Timing phases (#3923). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { serve, type Servers } from "../src/http/server.ts";
8 import { newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
9 import { seedAdmin } from "./helpers/admin.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11 import { proveAll } from "./helpers/verified.ts";
12
13 quietLogs();
14
15 const PASSWORD = "alice-login-secret";
16 const TOKEN = "octo-origin-secret-9f2a";
17 const auth = `Basic ${Buffer.from(`alice:${PASSWORD}`).toString("base64")}`;
18
19 let dataDir: string;
20 let app: App;
21 let servers: Servers;
22 let fake: FakeForge;
23 let originId = "";
24
25 beforeAll(async () => {
26 dataDir = await mkdtemp(join(tmpdir(), "forge-api-origin-repos-timing-"));
27 const state = newFakeState("forgejo", "octo", TOKEN);
28 state.repos.push({
29 id: 41,
30 fullName: "octo/widgets",
31 cloneUrl: "http://127.0.0.1:1/octo/widgets.git",
32 defaultBranch: "main",
33 });
34 fake = serveFakeForge(state);
35 app = createApp({ dataDir, port: 0 });
36 servers = await serve(app);
37 await seedAdmin(app, { username: "alice", email: "[email protected]", password: PASSWORD });
38 proveAll(app);
39 const connected = await fetch(`${servers.url}/api/v1/user/origins`, {
40 method: "POST",
41 headers: { authorization: auth, "content-type": "application/json" },
42 body: JSON.stringify({ host: fake.url, token: TOKEN }),
43 });
44 originId = String(((await connected.json()) as { id: string }).id);
45 });
46
47 afterAll(async () => {
48 fake.stop();
49 await servers.stop();
50 app.close();
51 await rm(dataDir, { recursive: true, force: true });
52 });
53
54 async function get(path: string): Promise<{ status: number; names: string[]; body: string }> {
55 const res = await fetch(`${servers.url}${path}`, { headers: { authorization: auth } });
56 const body = await res.text();
57 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
58 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
59 }
60
61 test("listing an origin's repositories names session, origin and render", async () => {
62 const got = await get(`/api/v1/user/origins/${originId}/repos?refresh=1`);
63 expect(got.status).toBe(200);
64 expect(got.names).toEqual(["session", "origin", "render"]);
65 expect(JSON.parse(got.body).map((r: { fullName: string }) => r.fullName)).toEqual([
66 "octo/widgets",
67 ]);
68 });
69
70 test("an unknown origin still answers 404 and names session and origin", async () => {
71 const got = await get("/api/v1/user/origins/nope/repos");
72 expect(got.status).toBe(404);
73 expect(got.names).toEqual(["session", "origin"]);
74 });
75
76 test("an unreachable origin answers 502 naming session and origin", async () => {
77 app.db.query("DELETE FROM origin_repo WHERE origin_id = ?1").run(originId);
78 fake.stop();
79 const got = await get(`/api/v1/user/origins/${originId}/repos?refresh=1`);
80 expect(got.status).toBe(502);
81 expect(JSON.parse(got.body).error).toBe("origin_unreachable");
82 expect(got.names).toEqual(["session", "origin"]);
83 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs