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

A migration job's read and an origin mirror job's read name their Server-Timing phases #5024

Merged opened this

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

Merged

scout/forge:scout/5013-a-migration-job-s-read-and-an-origin-mir → main 1 commit fork

  • Merged. scout/5013-a-migration-job-s-read-and-an-origin-mir was merged into main as 376b906.

Review

Sign in to review this change.
2 files changed +116 −8
src/http/api.ts +13 −8
@@ -4888,16 +4888,16 @@ export function apiRoutes(router: Router<Handler>): void {
48884888 });
48894889
48904890 router.add("GET", "/api/v1/user/origins/:id/mirror/:job", (ctx) => {
4891 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
48914892 const denied = requireAccount(ctx, { adminToken: true });
48924893 if (denied) return denied;
4893 const job = mirrorJobFor(
4894 ctx.app,
4895 ctx.auth.user,
4896 ctx.params.id!,
4897 ctx.params.job!,
4894 const job = timePhase(ctx.request, "job", () =>
4895 mirrorJobFor(ctx.app, ctx.auth.user, ctx.params.id!, ctx.params.job!),
48984896 );
48994897 if (!job.ok) return respondAccess(job.error, 404);
4900 return json({ job: jobJson(ctx, job.value) });
4898 return timePhase(ctx.request, "render", () =>
4899 json({ job: jobJson(ctx, job.value) }),
4900 );
49014901 });
49024902
49034903 // The same job as a live stream, so a mirror can be watched rather than
@@ -5383,9 +5383,14 @@ export function apiRoutes(router: Router<Handler>): void {
53835383 });
53845384
53855385 router.add("GET", "/api/v1/migrate/:job", (ctx) => {
5386 const found = migrationJobFor(ctx.auth.user, ctx.params.job!);
5386 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
5387 const found = timePhase(ctx.request, "job", () =>
5388 migrationJobFor(ctx.auth.user, ctx.params.job!),
5389 );
53875390 if (!found.ok) return respondAccess(found.error, 404);
5388 return json({ job: migrationJson(ctx, found.value) });
5391 return timePhase(ctx.request, "render", () =>
5392 json({ job: migrationJson(ctx, found.value) }),
5393 );
53895394 });
53905395
53915396 router.add("GET", "/api/v1/migrate/:job/stream", (ctx) => {
test/job-read-timing.test.ts added +103
@@ -0,0 +1,103 @@
1 /** GET /api/v1/migrate/:job and …/origins/:id/mirror/:job name their phases in Server-Timing (#5013). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtempSync, rmSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createOrigin } from "../src/core/origins.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { createJob } from "../src/origins/jobs.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let root = "";
16 let app: App;
17 let servers: Servers;
18 let cookie = "";
19 let userId = "";
20 let originId = "";
21
22 beforeAll(async () => {
23 root = mkdtempSync(join(tmpdir(), "forge-job-timing-"));
24 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
25 const u = await createUser(app.db, {
26 origin: "seed",
27 trusted: true,
28 username: "alice",
29 email: "[email protected]",
30 password: "a-good-passphrase",
31 adminIfFirst: true,
32 });
33 if (!u.ok) throw new Error(u.error);
34 userId = u.value.id;
35 const made = createOrigin(app.db, root, {
36 userId,
37 kind: "forgejo",
38 apiUrl: "https://git.example.test/api/v1",
39 webUrl: "https://git.example.test",
40 login: "alice",
41 token: "t0ken",
42 });
43 if (!made.ok) throw new Error(made.error);
44 originId = made.value.id;
45 servers = await serve(app);
46 const signin = await fetch(`${servers.url}/signin`, {
47 method: "POST",
48 headers: { origin: servers.url },
49 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
50 redirect: "manual",
51 });
52 cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
53 });
54 afterAll(() => {
55 servers.stop();
56 app.close();
57 rmSync(root, { recursive: true, force: true });
58 });
59
60 function names(res: Response): string[] {
61 return (res.headers.get("server-timing") ?? "")
62 .split(", ")
63 .map((p) => p.split(";")[0]!)
64 .filter((n) => ["session", "job", "render"].includes(n));
65 }
66
67 test("GET /api/v1/migrate/:job names session, job, render in order", async () => {
68 const job = createJob("", "octo/streamed", { kind: "migrate", actorId: userId });
69 const res = await fetch(`${servers.url}/api/v1/migrate/${job.id}`, { headers: { cookie } });
70 expect(res.status).toBe(200);
71 await res.text();
72 expect(names(res)).toEqual(["session", "job", "render"]);
73 });
74
75 test("a refused migrate read still names session", async () => {
76 const res = await fetch(`${servers.url}/api/v1/migrate/nope`, { headers: { cookie } });
77 expect(res.status).toBe(404);
78 await res.text();
79 expect(names(res)[0]).toBe("session");
80 const anon = await fetch(`${servers.url}/api/v1/migrate/nope`);
81 expect(anon.status).toBe(401);
82 await anon.text();
83 expect(names(anon)[0]).toBe("session");
84 });
85
86 test("GET …/origins/:id/mirror/:job names session, job, render in order", async () => {
87 const job = createJob(originId, "octo/streamed", { actorId: userId });
88 const res = await fetch(`${servers.url}/api/v1/user/origins/${originId}/mirror/${job.id}`, { headers: { cookie } });
89 expect(res.status).toBe(200);
90 await res.text();
91 expect(names(res)).toEqual(["session", "job", "render"]);
92 });
93
94 test("a refused mirror read still names session", async () => {
95 const res = await fetch(`${servers.url}/api/v1/user/origins/${originId}/mirror/nope`, { headers: { cookie } });
96 expect(res.status).toBe(404);
97 await res.text();
98 expect(names(res)[0]).toBe("session");
99 const anon = await fetch(`${servers.url}/api/v1/user/origins/${originId}/mirror/nope`);
100 expect([401, 403]).toContain(anon.status);
101 await anon.text();
102 expect(names(anon)[0]).toBe("session");
103 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs