|
1 | 1 | import { describe, expect, test } from "bun:test"; |
2 | 2 | import type { MiddlewareHandler } from "hono"; |
3 | | -import type { AppVariables } from "../src/auth/guards"; |
| 3 | +import type { AppVariables, AuthenticatedActor } from "../src/auth/guards"; |
4 | 4 | import type { ComputerGateway } from "../src/computer/gateway"; |
5 | 5 | import type { PolicyStore } from "../src/computer/policy-store"; |
6 | 6 | import { createComputerRoutes } from "../src/computer/routes"; |
@@ -40,3 +40,95 @@ describe("computer routes", () => { |
40 | 40 | expect(requestedBotIds).toEqual(["bot-17"]); |
41 | 41 | }); |
42 | 42 | }); |
| 43 | + |
| 44 | +/** |
| 45 | + * The fleet listing is the one route here that is not about the Bot in its path. |
| 46 | + * |
| 47 | + * `:botId` is ignored and the handler returns every computer, so a signed-in person asking about a |
| 48 | + * Bot they own learned every Bot id in the deployment and whether its computer was running, |
| 49 | + * private coworkers included. Being signed in is not the question; administering the deployment is. |
| 50 | + */ |
| 51 | +const member: AuthenticatedActor = { |
| 52 | + id: "user-1", |
| 53 | + email: "member@openbot.test", |
| 54 | + role: "user", |
| 55 | +}; |
| 56 | + |
| 57 | +const administrator: AuthenticatedActor = { |
| 58 | + id: "admin-1", |
| 59 | + email: "admin@openbot.test", |
| 60 | + role: "admin", |
| 61 | +}; |
| 62 | + |
| 63 | +function asActor( |
| 64 | + actor: AuthenticatedActor, |
| 65 | +): MiddlewareHandler<{ Variables: AppVariables }> { |
| 66 | + return async (context, next) => { |
| 67 | + context.set("actor", actor); |
| 68 | + await next(); |
| 69 | + }; |
| 70 | +} |
| 71 | + |
| 72 | +function appFor(actor: AuthenticatedActor, computers: () => Promise<unknown>) { |
| 73 | + let listed = 0; |
| 74 | + const countingGateway = { |
| 75 | + async computers() { |
| 76 | + listed += 1; |
| 77 | + return computers(); |
| 78 | + }, |
| 79 | + } as ComputerGateway; |
| 80 | + |
| 81 | + return { |
| 82 | + app: createComputerRoutes( |
| 83 | + countingGateway, |
| 84 | + {} as PolicyStore, |
| 85 | + asActor(actor), |
| 86 | + // Permissive. Whether this person may act as the Bot in the path is a different question with |
| 87 | + // its own suite, and `:botId` is not what this route answers about anyway. |
| 88 | + async () => true, |
| 89 | + ), |
| 90 | + listed: () => listed, |
| 91 | + }; |
| 92 | +} |
| 93 | + |
| 94 | +describe("computer fleet listing", () => { |
| 95 | + test("refuses a signed-in user the fleet, and does not ask the gateway", async () => { |
| 96 | + const { app, listed } = appFor(member, async () => ({ |
| 97 | + isolation: "per-bot", |
| 98 | + computers: [ |
| 99 | + { botId: "private-coworker", running: true, startedAt: null }, |
| 100 | + ], |
| 101 | + })); |
| 102 | + |
| 103 | + const response = await app.request("http://openbot.test/any-bot/computers"); |
| 104 | + |
| 105 | + expect(response.status).toBe(403); |
| 106 | + await expect(response.json()).resolves.toEqual({ |
| 107 | + error: "Administrator access required.", |
| 108 | + }); |
| 109 | + // Refused before the gateway is asked: a check that runs after the fleet has been read is not a |
| 110 | + // check, it is a filter on the response. |
| 111 | + expect(listed()).toBe(0); |
| 112 | + }); |
| 113 | + |
| 114 | + test("lets an administrator see the fleet", async () => { |
| 115 | + const fleet = { |
| 116 | + isolation: "per-bot" as const, |
| 117 | + computers: [ |
| 118 | + { |
| 119 | + botId: "private-coworker", |
| 120 | + running: true, |
| 121 | + startedAt: "2026-08-20T00:00:00.000Z", |
| 122 | + egress: null, |
| 123 | + }, |
| 124 | + ], |
| 125 | + }; |
| 126 | + const { app, listed } = appFor(administrator, async () => fleet); |
| 127 | + |
| 128 | + const response = await app.request("http://openbot.test/any-bot/computers"); |
| 129 | + |
| 130 | + expect(response.status).toBe(200); |
| 131 | + await expect(response.json()).resolves.toEqual(fleet); |
| 132 | + expect(listed()).toBe(1); |
| 133 | + }); |
| 134 | +}); |
0 commit comments