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

The organization reads name their Server-Timing phases: an org, its teams, a team's members, the caller's orgs #5179

Merged opened this

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

Merged

scout/forge:scout/4983-the-organization-reads-name-their-server → main 2 commits fork

  • Merged. scout/4983-the-organization-reads-name-their-server was merged into main as 71de37a.

Review

Sign in to review this change.
2 files changed +89 −11
src/http/api.ts +24 −11
@@ -1979,10 +1979,13 @@ export function apiRoutes(router: Router<Handler>): void {
19791979 });
19801980
19811981 router.add("GET", "/api/v1/user/orgs", (ctx) => {
1982 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
19821983 if (!ctx.auth.user) return unauthorized();
1983 return json(
1984 listOrgsForUser(ctx.app.db, ctx.auth.user.id).map(publicProfile),
1984 const user = ctx.auth.user;
1985 const orgs = timePhase(ctx.request, "orgs", () =>
1986 listOrgsForUser(ctx.app.db, user.id).map(publicProfile),
19851987 );
1988 return timePhase(ctx.request, "render", () => json(orgs));
19861989 });
19871990
19881991 // SSH keys — what the SSH listener authenticates against. Public keys are
@@ -2217,22 +2220,28 @@ export function apiRoutes(router: Router<Handler>): void {
22172220 });
22182221
22192222 router.add("GET", "/api/v1/orgs/:org", (ctx) => {
2223 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
22202224 const org = getOrg(ctx.app.db, ctx.params.org!);
22212225 if (!org) return notFound();
2222 return json({
2223 id: org.id,
2224 ...publicProfile(org),
2225 teams: listTeamsVisibleTo(ctx.app.db, org.id, ctx.auth.user),
2226 });
2226 const teams = timePhase(ctx.request, "org", () =>
2227 listTeamsVisibleTo(ctx.app.db, org.id, ctx.auth.user),
2228 );
2229 return timePhase(ctx.request, "render", () =>
2230 json({ id: org.id, ...publicProfile(org), teams }),
2231 );
22272232 });
22282233
22292234 router.add("GET", "/api/v1/orgs/:org/teams", (ctx) => {
2235 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
22302236 const org = getOrg(ctx.app.db, ctx.params.org!);
22312237 if (!org) return notFound();
22322238 const window = keyedWindow(ctx);
22332239 if (window instanceof Response) return window;
2234 return json(
2235 keyedPage(listTeamsVisibleTo(ctx.app.db, org.id, ctx.auth.user), (t) => t.name, window),
2240 const teams = timePhase(ctx.request, "teams", () =>
2241 listTeamsVisibleTo(ctx.app.db, org.id, ctx.auth.user),
2242 );
2243 return timePhase(ctx.request, "render", () =>
2244 json(keyedPage(teams, (t) => t.name, window)),
22362245 );
22372246 });
22382247
@@ -2264,13 +2273,17 @@ export function apiRoutes(router: Router<Handler>): void {
22642273 });
22652274
22662275 router.add("GET", "/api/v1/orgs/:org/teams/:team/members", (ctx) => {
2276 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
22672277 const found = loadTeam(ctx);
22682278 if ("error" in found) return found.error;
22692279 if (!teamVisible(ctx, found.org, found.team)) return notFound();
22702280 const window = keyedWindow(ctx);
22712281 if (window instanceof Response) return window;
2272 return json(
2273 keyedPage(listTeamMembers(ctx.app.db, found.team.id).map(publicProfile), (u) => u.username, window),
2282 const members = timePhase(ctx.request, "members", () =>
2283 listTeamMembers(ctx.app.db, found.team.id).map(publicProfile),
2284 );
2285 return timePhase(ctx.request, "render", () =>
2286 json(keyedPage(members, (u) => u.username, window)),
22742287 );
22752288 });
22762289
test/org-timing.test.ts added +65
@@ -0,0 +1,65 @@
1 /** The organization reads name their Server-Timing phases. */
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 { seedAdmin } from "./helpers/admin.ts";
9
10 let dataDir = "";
11 let app: App, servers: Servers, bearer = "";
12
13 const auth = () => ({ authorization: `Bearer ${bearer}`, "content-type": "application/json" });
14 const post = (path: string, body: unknown) =>
15 fetch(`${servers.url}${path}`, { method: "POST", headers: auth(), body: JSON.stringify(body) });
16
17 beforeAll(async () => {
18 dataDir = await mkdtemp(join(tmpdir(), "forge-otime-"));
19 app = createApp({ dataDir, port: 0 });
20 servers = await serve(app);
21 await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
22 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
23 method: "POST",
24 headers: { "content-type": "application/json" },
25 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
26 });
27 bearer = ((await minted.json()) as { token: string }).token;
28 expect((await post("/api/v1/orgs", { name: "acme" })).status).toBe(201);
29 expect((await post("/api/v1/orgs/acme/teams", { name: "dev" })).status).toBe(201);
30 });
31
32 afterAll(async () => {
33 servers.stop();
34 app.close();
35 await rm(dataDir, { recursive: true, force: true });
36 });
37
38 async function phases(path: string, status = 200): Promise<string[]> {
39 const res = await fetch(`${servers.url}${path}`, { headers: auth() });
40 await res.arrayBuffer();
41 expect(res.status).toBe(status);
42 return (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!).filter((n) => n !== "app" && n !== "db");
43 }
44
45 test("an org names session, org, render", async () => {
46 expect(await phases("/api/v1/orgs/acme")).toEqual(["session", "org", "render"]);
47 });
48
49 test("an org's teams name session, teams, render", async () => {
50 expect(await phases("/api/v1/orgs/acme/teams")).toEqual(["session", "teams", "render"]);
51 });
52
53 test("a team's members name session, members, render", async () => {
54 expect(await phases("/api/v1/orgs/acme/teams/dev/members")).toEqual(["session", "members", "render"]);
55 });
56
57 test("the caller's orgs name session, orgs, render", async () => {
58 expect(await phases("/api/v1/user/orgs")).toEqual(["session", "orgs", "render"]);
59 });
60
61 test("a refused read still names session", async () => {
62 expect((await phases("/api/v1/orgs/nope", 404))[0]).toBe("session");
63 expect((await phases("/api/v1/orgs/nope/teams", 404))[0]).toBe("session");
64 expect((await phases("/api/v1/orgs/acme/teams/nope/members", 404))[0]).toBe("session");
65 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs