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

The API's forge family reads name their Server-Timing phases: session, read, render #3950

Merged opened this

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

Merged

scout/forge:scout/3937-the-api-s-forge-family-reads-name-their → main 3 commits fork

  • Merged. scout/3937-the-api-s-forge-family-reads-name-their was merged into main as 6feecfb.

Review

Sign in to review this change.
2 files changed +88 −4
src/http/api.ts +11 −4
@@ -4966,10 +4966,11 @@ export function apiRoutes(router: Router<Handler>): void {
49664966 // when anything last happened, only to its parent's secret. Nothing else;
49674967 // 404 when not spawned.
49684968 router.add("GET", "/api/v1/forge", async (ctx) => {
4969 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
49694970 const shown = ctx.request.headers.get(PARENT_HEADER) ?? undefined;
4970 const state = await forgeState(ctx.app, shown);
4971 const state = await timePhaseAsync(ctx.request, "read", () => forgeState(ctx.app, shown));
49714972 if (!state) return apiError("not_found", 404, "this forge was not spawned");
4972 return json(state);
4973 return timePhase(ctx.request, "render", () => json(state));
49734974 });
49744975
49754976 // A child's signed heartbeat (docs/SPAWN.md): counts and version, never
@@ -4984,9 +4985,13 @@ export function apiRoutes(router: Router<Handler>): void {
49844985
49854986 // The family (#425): the parent for anyone, the children for an admin.
49864987 router.add("GET", "/api/v1/family", async (ctx) => {
4988 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
49874989 const denied = ctx.auth.user === undefined ? undefined : requireScope(ctx, "ro");
49884990 if (denied) return denied;
4989 return json(await familyView(ctx.app, ctx.auth.user?.isAdmin === true));
4991 const view = await timePhaseAsync(ctx.request, "read", () =>
4992 familyView(ctx.app, ctx.auth.user?.isAdmin === true),
4993 );
4994 return timePhase(ctx.request, "render", () => json(view));
49904995 });
49914996
49924997 // What small, medium and large mean: public, and cacheable by ETag.
@@ -5000,10 +5005,12 @@ export function apiRoutes(router: Router<Handler>): void {
50005005
50015006 // A forge's children (docs/SPAWN.md): an admin's, planned before made.
50025007 router.add("GET", "/api/v1/forges", async (ctx) => {
5008 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
50035009 const denied = requireScope(ctx, "ro");
50045010 if (denied) return denied;
50055011 if (!ctx.auth.user?.isAdmin) return forbidden();
5006 return json({ items: await listChildren(ctx.app), next_cursor: null });
5012 const items = await timePhaseAsync(ctx.request, "read", () => listChildren(ctx.app));
5013 return timePhase(ctx.request, "render", () => json({ items, next_cursor: null }));
50075014 });
50085015
50095016 // `dryRun` is true unless it is `false`: the plan, and nothing is made.
test/api-family-timing.test.ts added +77
@@ -0,0 +1,77 @@
1 /** The API's forge family reads name their Server-Timing phases (#3937). */
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 { createSession } from "../src/core/sessions.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { quietLogs } from "./helpers/globals.ts";
11
12 quietLogs();
13
14 let dataDir: string;
15 let app: App;
16 let servers: Servers;
17 let cookie = "";
18 const cells = process.env["FORGE_CELLS"];
19
20 beforeAll(async () => {
21 process.env["FORGE_CELLS"] = "off";
22 dataDir = await mkdtemp(join(tmpdir(), "forge-api-family-timing-"));
23 app = createApp({ dataDir, port: 0 });
24 servers = await serve(app);
25 const made = 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 (!made.ok) throw new Error(String(made.error));
34 cookie = `forge_session=${createSession(app.db, made.value.id).token}`;
35 });
36
37 afterAll(async () => {
38 await servers.stop();
39 app.close();
40 if (cells === undefined) delete process.env["FORGE_CELLS"];
41 else process.env["FORGE_CELLS"] = cells;
42 await rm(dataDir, { recursive: true, force: true });
43 });
44
45 async function phases(path: string, withCookie = true) {
46 const res = await fetch(`${servers.url}${path}`, { headers: withCookie ? { cookie } : {} });
47 const body = await res.text();
48 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
49 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
50 }
51
52 test("GET /api/v1/family names session, read, render", async () => {
53 const got = await phases("/api/v1/family");
54 expect(got.status).toBe(200);
55 expect(got.names).toEqual(["session", "read", "render"]);
56 expect(typeof JSON.parse(got.body)).toBe("object");
57 });
58
59 test("GET /api/v1/forges names session, read, render for an admin", async () => {
60 const got = await phases("/api/v1/forges");
61 expect(got.status).toBe(200);
62 expect(got.names).toEqual(["session", "read", "render"]);
63 expect(JSON.parse(got.body).next_cursor).toBeNull();
64 });
65
66 test("GET /api/v1/forges refuses a stranger and still names session", async () => {
67 const got = await phases("/api/v1/forges", false);
68 expect(got.status).toBeGreaterThanOrEqual(401);
69 expect(got.status).toBeLessThanOrEqual(403);
70 expect(got.names).toEqual(["session"]);
71 });
72
73 test("GET /api/v1/forge on an unspawned forge 404s naming session and read", async () => {
74 const got = await phases("/api/v1/forge");
75 expect(got.status).toBe(404);
76 expect(got.names).toEqual(["session", "read"]);
77 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs