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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5013-a-migration-job-s-read-and-an-origin-mir → main 1 commit fork
2 files
changed +116
−8
src/http/api.ts +13 −8
| @@ -4888,16 +4888,16 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4888 | 4888 | }); |
| 4889 | 4889 | |
| 4890 | 4890 | router.add("GET", "/api/v1/user/origins/:id/mirror/:job", (ctx) => { |
| 4891 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 4891 | 4892 | const denied = requireAccount(ctx, { adminToken: true }); |
| 4892 | 4893 | 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!), | |
| 4898 | 4896 | ); |
| 4899 | 4897 | 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 | ); | |
| 4901 | 4901 | }); |
| 4902 | 4902 | |
| 4903 | 4903 | // 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 { | ||
| 5383 | 5383 | }); |
| 5384 | 5384 | |
| 5385 | 5385 | 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 | ); | |
| 5387 | 5390 | 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 | ); | |
| 5389 | 5394 | }); |
| 5390 | 5395 | |
| 5391 | 5396 | 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 | }); |
new above
offline