import { HttpApiBuilder } from "effect/unstable/httpapi"; import { HttpRouter, HttpServer } from "effect/unstable/http"; import { describe, expect, it } from "@effect/vitest"; import { Context, Data, Effect, Layer, type Scope } from "effect"; import { Subject, Tenant, collectTables, createExecutor, type AdminSubject, type Executor, type ExecutorAdmin, } from "@executor-js/sdk"; import { createSqliteTestFumaDb, type SqliteTestFumaDb } from "@executor-js/sdk/testing"; import { resetSubjectTouchCache, touchSubject } from "@executor-js/sdk/host-internal"; import { AdminUsersHttpApi, AdminUsersForbidden, AdminUsersUnauthorized } from "./api"; import { AdminUsersHandlers } from "./handlers"; import { AdminUsersProvider, type AdminUsersHeaders } from "./service"; import { getUser, listUserConnections, listUsers, listUsersWithConnections, platformViewOf, type AdminIdentityDirectory, type AdminUserDirectory, } from "./reads"; // --------------------------------------------------------------------------- // The admin users HTTP surface, over a real SQLite-backed platform view. // // The reads themselves (tenant reach, the bigint `last_seen_at` round-trip, the // field allowlist) are proven in the SDK's `platform-view.test.ts`. What this // file proves is the HTTP EDGE on top of them: // - the public vocabulary + wire shapes the contract promises, // - the authorization outcomes (401 vs 403) a host's provider produces, // - that a tenant's admin plane cannot see another tenant's rows, // - that no credential-bearing field survives the projection. // // The provider seam is stubbed the way each host's real one behaves, so the // authz assertions are about the CONTRACT (which status a refusal renders), // not about WorkOS or Better Auth. // --------------------------------------------------------------------------- const TENANT_A = "org_a"; const TENANT_B = "org_b"; const USER_A1 = "user_a1"; const USER_A2 = "user_a2"; const USER_B1 = "user_b1"; /** Every field that could resolve, or help resolve, a credential. NONE may * appear anywhere in an admin HTTP response — enumerated so a future column * addition has to be argued with this list. Mirrors the SDK's own list. */ const FORBIDDEN_FIELDS = [ "item_ids", "itemIds", "refresh_item_id", "refreshItemId", "provider", "provider_state", "providerState", "oauth_token_url", "oauthTokenUrl", "template", "client_secret", "clientSecret", "secret", "token", // The internal partition keys: the public vocabulary is users/owners, so // neither the raw column name nor the tenant may leak onto the wire. "subject", "tenant", // The upstream-derived halves of a stored health verdict. These are not // CREDENTIALS — they are the provider's own response content: the account // identity extracted from `identityField` (another user's email at the // provider), a raw diagnostic that can quote an upstream error body, and a // sample of the real response body. `lastHealth` is an allowed COLUMN, which // is exactly why these need naming: the per-column allowlist passed while the // whole nested object rode along inside it. `AdminConnectionHealth` narrows // it to status + checkedAt; this list is what keeps it narrow. "identity", "detail", "responseSample", "httpStatus", ] as const; // The bodies below acquire a scoped web handler, so `R` carries `Scope` rather // than being erased; `Effect.scoped` closes it when the body finishes. const withDb = ( body: (db: SqliteTestFumaDb) => Effect.Effect, ): Effect.Effect => Effect.scoped( Effect.acquireUseRelease( Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() })), body, (db) => Effect.promise(() => db.close()), ), ); const insertConnection = ( db: SqliteTestFumaDb, row: { readonly rowId: string; readonly tenant: string; readonly owner: string; readonly subject: string; readonly integration: string; readonly name: string; readonly oauthScope?: string | null; }, ): Effect.Effect => Effect.promise(async () => { await db.client.execute({ sql: `INSERT INTO connection ( row_id, tenant, owner, subject, integration, name, template, provider, item_ids, refresh_item_id, oauth_scope, last_health, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, args: [ row.rowId, row.tenant, row.owner, row.subject, row.integration, row.name, "oauth2", "memory", // Deliberately secret-bearing: the assertions below prove these never // reach an HTTP response. JSON.stringify({ token: `SECRET-item-${row.rowId}` }), `SECRET-refresh-${row.rowId}`, row.oauthScope ?? null, // A REAL stored health verdict, including the parts that come from the // upstream provider's own response. `identity` is the value pulled from // the configured `identityField` (typically the account's email AT the // provider), `detail` is a raw diagnostic that can quote an upstream // body, and `responseSample` is a bounded sample of the actual response // body's scalar leaves. All three are legitimate on the product plane, // where the caller owns the credential — and none may cross onto the // ADMIN plane, which reports on other people's connections. Seeded here // so the assertions below have ground truth to catch rather than // asserting the absence of something that was never present. JSON.stringify({ status: "healthy", checkedAt: 1_700_000_000_000, httpStatus: 200, identity: `UPSTREAM-identity-${row.rowId}@provider.test`, detail: `UPSTREAM-detail-${row.rowId}`, responseSample: [ { path: "user.email", value: `UPSTREAM-sample-${row.rowId}@provider.test` }, { path: "user.login", value: `UPSTREAM-login-${row.rowId}` }, ], }), Date.now(), Date.now(), ], }); }); /** Two users with connections under tenant A, plus a whole separate tenant B * that A's admin plane must never see. */ const seed = (db: SqliteTestFumaDb): Effect.Effect => Effect.gen(function* () { // `touchSubject` keeps a process-local memory of principals it already // filed, so a second seed against a FRESH database would be skipped as a // repeat sighting and leave the table empty. Each seed is a new world. resetSubjectTouchCache(); yield* touchSubject(db.db, { tenant: TENANT_A, externalId: USER_A1 }); yield* touchSubject(db.db, { tenant: TENANT_A, externalId: USER_A2 }); yield* touchSubject(db.db, { tenant: TENANT_B, externalId: USER_B1 }); yield* insertConnection(db, { rowId: "c-a1-gh", tenant: TENANT_A, owner: "user", subject: USER_A1, integration: "github", name: "personal", oauthScope: "repo read:user", }); yield* insertConnection(db, { rowId: "c-a2-linear", tenant: TENANT_A, owner: "user", subject: USER_A2, integration: "linear", name: "work", }); yield* insertConnection(db, { rowId: "c-b1-gh", tenant: TENANT_B, owner: "user", subject: USER_B1, integration: "github", name: "b-secret", oauthScope: "admin:org", }); }); const platformExecutorFor = (db: SqliteTestFumaDb, tenant: string): Effect.Effect => createExecutor({ tenant: Tenant.make(tenant), db: db.db, onElicitation: "accept-all", platformView: true, }).pipe(Effect.orDie); /** A product-view executor: bound to one subject, no platform view at all. */ const productExecutorFor = (db: SqliteTestFumaDb, tenant: string): Effect.Effect => createExecutor({ tenant: Tenant.make(tenant), subject: Subject.make(USER_A1), db: db.db, onElicitation: "accept-all", }).pipe(Effect.orDie); /** * A stub `AdminUsersProvider` shaped like a host's real one: it authorizes off * the request headers, then delegates to the shared reads. `authorize` returns * the tenant an authorized caller may read, or a refusal — exactly the decision * cloud makes from an org key / admin session and self-host from a Better Auth * member. */ const stubProvider = ( executorFor: (tenant: string) => Effect.Effect, authorize: ( headers: AdminUsersHeaders, ) => Effect.Effect, directory?: AdminIdentityDirectory | AdminUserDirectory, ) => Layer.succeed(AdminUsersProvider)({ listUsers: (headers, options) => authorize(headers).pipe( Effect.flatMap(executorFor), Effect.flatMap((executor) => platformViewOf(executor).pipe( Effect.flatMap((admin) => listUsers(admin, options, directory)), ), ), ), listUsersWithConnections: (headers, options) => authorize(headers).pipe( Effect.flatMap(executorFor), Effect.flatMap((executor) => platformViewOf(executor).pipe( Effect.flatMap((admin) => listUsersWithConnections(admin, options, directory)), ), ), ), listUserConnections: (headers, externalId) => authorize(headers).pipe( Effect.flatMap(executorFor), Effect.flatMap((executor) => platformViewOf(executor).pipe( Effect.flatMap((admin) => listUserConnections(admin, externalId)), ), ), ), getUser: (headers, identifier) => authorize(headers).pipe( Effect.flatMap(executorFor), Effect.flatMap((executor) => platformViewOf(executor).pipe( Effect.flatMap((admin) => getUser(admin, identifier, directory)), ), ), ), }); /** * The authorization the hosts implement, reduced to its decision table: * an org-scoped key (or admin session) names its tenant; a plain member or a * user-scoped key is forbidden; no credential at all is unauthorized. */ const headerAuthorize = (headers: AdminUsersHeaders) => { const credential = headers["authorization"]; if (!credential) return Effect.fail(new AdminUsersUnauthorized()); if (credential === "Bearer org_a_key") return Effect.succeed(TENANT_A); if (credential === "Bearer org_b_key") return Effect.succeed(TENANT_B); // A valid user-scoped key / plain member session: authentic, but this plane // serves the whole tenant and they name one member. return Effect.fail(new AdminUsersForbidden()); }; // `HttpRouter.provideRequest` (not a plain `Layer.provide`) is what clears the // handlers' per-request `AdminUsersProvider` marker — the same reason the real // hosts mount the provider through a router middleware. const webHandlerFor = (provider: Layer.Layer) => Effect.acquireRelease( Effect.sync(() => HttpRouter.toWebHandler( HttpApiBuilder.layer(AdminUsersHttpApi).pipe( Layer.provide(AdminUsersHandlers), HttpRouter.provideRequest(provider), Layer.provideMerge(HttpServer.layerServices), Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), ), { disableLogger: true }, ), ), (web) => Effect.promise(() => web.dispose()), ); const get = ( web: { readonly handler: (request: Request, context: Context.Context) => Promise; }, path: string, credential?: string, ) => Effect.promise(() => web.handler( new Request(`http://localhost${path}`, { headers: credential ? { authorization: credential } : {}, }), Context.empty(), ), ); const jsonOf = (response: Response) => Effect.promise(() => response.json() as Promise); type UsersBody = { readonly users: ReadonlyArray<{ readonly externalId: string; readonly email: string | null; readonly displayName: string | null; }>; }; type ConnectionsBody = { readonly connections: ReadonlyArray<{ readonly integration: string; readonly name: string }>; }; type UserBody = { readonly user: { readonly externalId: string; readonly email: string | null; readonly displayName: string | null; readonly lastSeenAt: number | null; readonly status: string | null; readonly connections: ReadonlyArray<{ readonly integration: string; readonly oauthScope: string | null; readonly lastHealth: { readonly status: string } | null; }>; }; }; type UsersWithConnectionsBody = { readonly users: ReadonlyArray<{ readonly externalId: string; readonly email: string | null; readonly displayName: string | null; readonly lastSeenAt: number | null; readonly status: string | null; readonly connections: ReadonlyArray<{ readonly integration: string; readonly oauthScope: string | null; readonly lastHealth: { readonly status: string } | null; }>; }>; }; const ORG_A = "Bearer org_a_key"; /** * A host member directory that knows USER_A1 only. * * USER_A2 is deliberately absent — that is the real case of a member who left * the org while their connections remain, and the one an id-space mismatch would * also look like. `seen` records the ids the directory was asked for, so a test * can prove the join is ONE batched read of the returned page rather than a * lookup per user. */ const stubDirectory = (seen: string[][]): AdminIdentityDirectory => (externalIds) => { seen.push([...externalIds]); return Effect.succeed(new Map([[USER_A1, { email: "a1@users.test", displayName: "User A1" }]])); }; /** The email the stub directory knows USER_A1 by. Stored MIXED-CASE on * purpose: WorkOS preserves whatever casing a user was created with, so the * case-insensitivity rule has to hold against a non-normalized directory * value, not just a non-normalized query. */ const A1_EMAIL_STORED = "A1@Users.Test"; const A1_EMAIL = "a1@users.test"; /** * The two-way directory: the same USER_A1-only knowledge as `stubDirectory`, * plus the reverse resolver. * * USER_A2 stays absent in BOTH directions, so it doubles as the "member the * directory cannot name" case. `resolved` records every email the resolver was * asked for, so a test can prove the lookup normalizes before comparing. */ const stubUserDirectory = (options: { readonly seen?: string[][]; readonly resolved?: string[]; readonly searched?: string[]; }): AdminUserDirectory => ({ identities: (externalIds) => { options.seen?.push([...externalIds]); return Effect.succeed(new Map([[USER_A1, { email: A1_EMAIL_STORED, displayName: "User A1" }]])); }, resolveEmail: (email) => { options.resolved?.push(email); // Compares a NORMALIZED stored value, the rule both real hosts follow. return Effect.succeed(A1_EMAIL_STORED.toLowerCase() === email ? USER_A1 : null); }, search: (term) => { options.searched?.push(term); // The one member the directory knows, matched on the normalized email or // the display name — the substring rule both real hosts apply. const haystack = [A1_EMAIL_STORED.toLowerCase(), "user a1"]; return Effect.succeed(haystack.some((value) => value.includes(term)) ? [USER_A1] : []); }, }); /** The failure a host's directory raises — WorkOS or Better Auth being * unreachable. Typed rather than a bare `Error`, since the directory seam's * `unknown` error channel exists to carry each host's OWN error type. */ class DirectoryUnavailable extends Data.TaggedError("DirectoryUnavailable")<{ readonly message: string; }> {} /** A directory whose read fails, standing in for a WorkOS/Better Auth outage. */ const failingDirectory: AdminIdentityDirectory = () => Effect.fail(new DirectoryUnavailable({ message: "member directory unavailable" })); describe("admin users API", () => { it.effect("lists every user of the tenant for an authorized org caller", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const response = yield* get(web, "/admin/users", ORG_A); expect(response.status).toBe(200); const body = yield* jsonOf(response); expect( body.users.map((user) => user.externalId), "both of the tenant's users are reported", ).toEqual([USER_A1, USER_A2]); }), ), ); it.effect("reports a user's connections with access summary but no credential", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const response = yield* get(web, `/admin/users/${USER_A1}/connections`, ORG_A); expect(response.status).toBe(200); const body = yield* jsonOf(response); expect( body.connections.map((connection) => connection.integration), "the user's own connection is reported", ).toEqual(["github"]); expect(body.connections[0]?.name).toBe("personal"); }), ), ); it.effect("joins users with their connections for the dashboard view", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const response = yield* get(web, "/admin/users/with-connections", ORG_A); expect(response.status).toBe(200); const body = yield* jsonOf(response); expect( body.users.map((user) => [user.externalId, user.connections.map((c) => c.integration)]), "each user carries their own connections", ).toEqual([ [USER_A1, ["github"]], [USER_A2, ["linear"]], ]); // The access summary and health travel; `lastSeenAt` is a number the // dashboard can render (a raw SQL read would hand back a blob). expect(body.users[0]?.connections[0]?.oauthScope).toBe("repo read:user"); expect(body.users[0]?.connections[0]?.lastHealth?.status).toBe("healthy"); expect(typeof body.users[0]?.lastSeenAt).toBe("number"); expect(body.users[0]?.status, "no lifecycle state is recorded yet").toBeNull(); }), ), ); it.effect("never serves credential material on any admin response", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); for (const path of [ "/admin/users", "/admin/users/with-connections", `/admin/users/${USER_A1}/connections`, `/admin/users/${USER_A1}`, ]) { const response = yield* get(web, path, ORG_A); const raw = yield* Effect.promise(() => response.text()); for (const field of FORBIDDEN_FIELDS) { expect(raw, `${path} must not expose "${field}"`).not.toContain(`"${field}"`); } // The seeded secrets are the ground truth: nothing derived from a // credential may appear, whatever it is called. expect(raw, `${path} must not leak a stored secret`).not.toContain("SECRET-"); // And the same test for upstream ACCOUNT data, which is a different // kind of leak from a credential and was reaching the wire inside // `lastHealth`. Matching on the seeded marker rather than on field // names catches a rename, a re-nesting, or a stringified blob. expect(raw, `${path} must not leak upstream account data`).not.toContain("UPSTREAM-"); } }), ), ); it.effect("isolates tenants: an org caller sees nothing of another tenant", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const users = yield* jsonOf(yield* get(web, "/admin/users", ORG_A)); expect( users.users.map((user) => user.externalId), "tenant B's user is invisible to tenant A", ).not.toContain(USER_B1); // Naming another tenant's user directly returns nothing rather than // that user's connections — the tenant clause is never relaxed. const crossTenant = yield* jsonOf( yield* get(web, `/admin/users/${USER_B1}/connections`, ORG_A), ); expect(crossTenant.connections, "tenant B's connections stay in tenant B").toEqual([]); // And tenant B's own caller does see them — proving the emptiness above // is isolation, not a broken fixture. const ownView = yield* jsonOf( yield* get(web, `/admin/users/${USER_B1}/connections`, "Bearer org_b_key"), ); expect(ownView.connections.map((c) => c.name)).toEqual(["b-secret"]); }), ), ); it.effect("refuses a caller with no credential and a caller who is only a member", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const anonymous = yield* get(web, "/admin/users"); expect(anonymous.status, "no credential → unauthorized").toBe(401); // A user-scoped API key or plain member session: authentic, but this // plane serves the whole tenant. const member = yield* get(web, "/admin/users", "Bearer user_scoped_key"); expect(member.status, "a non-admin caller → forbidden").toBe(403); const memberJoined = yield* get(web, "/admin/users/with-connections", "Bearer user_key"); expect(memberJoined.status).toBe(403); const memberConnections = yield* get( web, `/admin/users/${USER_A1}/connections`, "Bearer user_key", ); expect(memberConnections.status).toBe(403); // The single-user read refuses on the same terms, and crucially it // refuses BEFORE looking: an unauthorized caller must not be able to // tell a real user from an absent one by the status they get back. expect((yield* get(web, `/admin/users/${USER_A1}`)).status).toBe(401); expect((yield* get(web, `/admin/users/${USER_A1}`, "Bearer user_key")).status).toBe(403); expect((yield* get(web, "/admin/users/nobody", "Bearer user_key")).status).toBe(403); }), ), ); // The response schema strips unknown fields on encode, so the HTTP assertions // above would still pass if the projection leaked one. Assert the projection // directly too: it is the layer that is supposed to be an allowlist, and both // layers should hold on their own. it.effect("projects only allowlisted fields, before the schema ever encodes", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const executor = yield* platformExecutorFor(db, TENANT_A); const admin = yield* platformViewOf(executor); const { connections } = yield* listUserConnections(admin, USER_A1); expect( Object.keys(connections[0] ?? {}).sort(), "the connection projection is exactly the allowlist", ).toEqual(["integration", "lastHealth", "name", "oauthScope", "owner"]); // `lastHealth` is an allowed column holding a nested object, so the // allowlist has to reach INSIDE it — a whole-object forward is what let // the provider's `identity` / `detail` / `responseSample` onto the // wire. The stored verdict here carries all of them (see the seed), so // this is a projection assertion, not a vacuous one. expect( Object.keys(connections[0]?.lastHealth ?? {}).sort(), "the health projection is exactly status + checkedAt", ).toEqual(["checkedAt", "status"]); expect(connections[0]?.lastHealth?.status, "and the verdict itself survives").toBe( "healthy", ); const { users } = yield* listUsers(admin, {}); expect( Object.keys(users[0] ?? {}).sort(), "the user projection is exactly the allowlist", ).toEqual(["createdAt", "displayName", "email", "externalId", "lastSeenAt", "status"]); }), ), ); it.effect("fails loudly when wired to a product-view executor", () => withDb((db) => Effect.gen(function* () { yield* seed(db); // A host that forgot `platformView: true` must not silently report an // empty tenant — that would read as "this owner has no users". const web = yield* webHandlerFor( stubProvider((tenant) => productExecutorFor(db, tenant), headerAuthorize), ); const response = yield* get(web, "/admin/users", ORG_A); expect(response.status, "a missing platform view is a server error").toBe(500); }), ), ); // ── Host identity join ──────────────────────────────────────────────────── it.effect("joins the host's email and name onto users the directory resolves", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const seen: string[][] = []; const web = yield* webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, stubDirectory(seen), ), ); const body = yield* jsonOf(yield* get(web, "/admin/users", ORG_A)); expect( body.users.map((user) => [user.externalId, user.email, user.displayName]), "the resolvable member is named; the unresolvable one reports absent identity", ).toEqual([ [USER_A1, "a1@users.test", "User A1"], [USER_A2, null, null], ]); // The join is a BATCH: one directory read for the whole page, carrying // every id at once — never one read per user. expect(seen, "one directory read, for exactly the page's ids").toEqual([ [USER_A1, USER_A2], ]); }), ), ); it.effect("joins identity onto the connections view too, over the same one read", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const seen: string[][] = []; const web = yield* webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, stubDirectory(seen), ), ); const body = yield* jsonOf( yield* get(web, "/admin/users/with-connections", ORG_A), ); expect(body.users.map((user) => [user.externalId, user.email])).toEqual([ [USER_A1, "a1@users.test"], [USER_A2, null], ]); expect(seen.length, "still one directory read for the page").toBe(1); }), ), ); // Identity is decoration on an operator view, not the answer: a directory // outage must cost the rows their names, never cost the operator the read. it.effect("still serves users when the host's identity lookup fails", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, failingDirectory, ), ); const response = yield* get(web, "/admin/users", ORG_A); expect(response.status, "a directory outage is not a failed request").toBe(200); const body = yield* jsonOf(response); expect( body.users.map((user) => [user.externalId, user.email, user.displayName]), "every user is still reported, simply unnamed", ).toEqual([ [USER_A1, null, null], [USER_A2, null, null], ]); }), ), ); // A host that cannot map its id space to a directory at all passes none. The // rows must still be complete — absent identity, not a missing field. it.effect("reports absent identity when the host wires no directory", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const body = yield* jsonOf(yield* get(web, "/admin/users", ORG_A)); expect(body.users.map((user) => [user.email, user.displayName])).toEqual([ [null, null], [null, null], ]); }), ), ); // Email and name are the ONLY fields this join is allowed to add. The // forbidden-field assertions above run without a directory, so re-run the // credential check with one wired: a directory is a new data source reaching // the response, and it must not become a hole in the allowlist. it.effect("adds only email and displayName to the allowlist", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const seen: string[][] = []; const web = yield* webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, stubUserDirectory({ seen }), ), ); for (const path of [ "/admin/users", "/admin/users/with-connections", // The single-user read joins identity through the same seam, so it // gets the same sweep — including the email-addressed form, whose // response is built on a directory value. `/admin/users/${USER_A1}`, `/admin/users/${encodeURIComponent(A1_EMAIL)}`, `/admin/users?email=${encodeURIComponent(A1_EMAIL)}`, ]) { const response = yield* get(web, path, ORG_A); const raw = yield* Effect.promise(() => response.text()); for (const field of FORBIDDEN_FIELDS) { expect(raw, `${path} must not expose "${field}"`).not.toContain(`"${field}"`); } expect(raw, `${path} must not leak a stored secret`).not.toContain("SECRET-"); } }), ), ); it.effect("pages the user list", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); const first = yield* jsonOf(yield* get(web, "/admin/users?limit=1", ORG_A)); expect(first.users.map((user) => user.externalId)).toEqual([USER_A1]); const second = yield* jsonOf( yield* get(web, "/admin/users?limit=1&offset=1", ORG_A), ); expect(second.users.map((user) => user.externalId)).toEqual([USER_A2]); }), ), ); // `offset` and `limit` are independently optional in the contract, so // `?offset=` alone is a request a consumer will make. Drizzle's SQLite // dialect emits OFFSET only alongside LIMIT and SQLite rejects a bare OFFSET, // so this used to 500 on every SQLite host — local, self-host and D1 alike. // The SDK's default page size is what makes it a legal query. it.effect("serves an offset with no limit rather than 500ing", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); for (const path of ["/admin/users?offset=1", "/admin/users/with-connections?offset=1"]) { const response = yield* get(web, path, ORG_A); expect(response.status, path).toBe(200); expect((yield* jsonOf(response)).users.map((user) => user.externalId)).toEqual( [USER_A2], ); } // Well past the end is an empty page, still a 200. const far = yield* get(web, "/admin/users?offset=25", ORG_A); expect(far.status).toBe(200); expect((yield* jsonOf(far)).users).toEqual([]); }), ), ); // A fraction decoded cleanly through `FiniteFromString` and reached the // driver as a `datatype mismatch` 500. A row count is an integer by nature, // so the contract rejects it up front — the same 400 a non-numeric value gets, // which is what the schema's comment always claimed. it.effect("400s a fractional limit or offset instead of failing in the driver", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize), ); for (const path of [ "/admin/users?limit=1.5", "/admin/users?offset=0.5", "/admin/users/with-connections?limit=1.5", // Still 400s for the reasons that already held. "/admin/users?limit=abc", "/admin/users?limit=0", "/admin/users?limit=501", ]) { expect((yield* get(web, path, ORG_A)).status, path).toBe(400); } // The integral neighbours are unaffected. expect((yield* get(web, "/admin/users?limit=1&offset=0", ORG_A)).status).toBe(200); }), ), ); // ------------------------------------------------------------------------- // Single-user reads, by opaque id and by email. // ------------------------------------------------------------------------- const singleUserWeb = (db: SqliteTestFumaDb, resolved?: string[]) => webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, stubUserDirectory(resolved ? { resolved } : {}), ), ); it.effect("returns one user with identity and connections in a single call", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* singleUserWeb(db); const response = yield* get(web, `/admin/users/${USER_A1}`, ORG_A); expect(response.status).toBe(200); const body = yield* jsonOf(response); // The whole point of the endpoint: identity AND connections together, // so a per-user check needs exactly one request. expect(body.user.externalId).toBe(USER_A1); expect(body.user.email).toBe(A1_EMAIL_STORED); expect(body.user.displayName).toBe("User A1"); expect(typeof body.user.lastSeenAt).toBe("number"); expect(body.user.connections.map((connection) => connection.integration)).toEqual([ "github", ]); expect(body.user.connections[0]?.oauthScope).toBe("repo read:user"); expect(body.user.connections[0]?.lastHealth?.status).toBe("healthy"); }), ), ); it.effect("finds the same user by email, case-insensitively", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const resolved: string[] = []; const web = yield* singleUserWeb(db, resolved); // Three spellings of one address: as stored, all-lower, and shouted. for (const spelling of [A1_EMAIL_STORED, A1_EMAIL, A1_EMAIL.toUpperCase()]) { const response = yield* get(web, `/admin/users/${encodeURIComponent(spelling)}`, ORG_A); expect(response.status, `${spelling} should resolve`).toBe(200); const body = yield* jsonOf(response); expect(body.user.externalId).toBe(USER_A1); expect(body.user.connections.map((connection) => connection.integration)).toEqual([ "github", ]); } // The seam normalizes before the resolver ever sees the value, so a // host implementation never has to repeat the rule for the QUERY side. expect(resolved).toEqual([A1_EMAIL, A1_EMAIL, A1_EMAIL]); }), ), ); it.effect("404s an unknown id and an unknown email, distinctly from 401 and 403", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* singleUserWeb(db); const unknownId = yield* get(web, "/admin/users/user_nobody", ORG_A); expect(unknownId.status).toBe(404); const unknownEmail = yield* get( web, `/admin/users/${encodeURIComponent("nobody@users.test")}`, ORG_A, ); expect(unknownEmail.status).toBe(404); // The distinct error is what lets a caller tell "no footprint" from // "bad credential" without parsing a message. Asserted on the encoded // body, which is what a consumer actually receives. expect(yield* Effect.promise(() => unknownId.text())).toContain("AdminUserNotFound"); // And 404 must not echo the identifier back. const raw = yield* Effect.promise(() => unknownEmail.text()); expect(raw).not.toContain("nobody@users.test"); }), ), ); it.effect("404s a user of ANOTHER tenant rather than revealing them", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* singleUserWeb(db); // B's user exists, but not for A — the same answer as a nonexistent id, // which is what keeps the admin plane tenant-isolated. expect((yield* get(web, `/admin/users/${USER_B1}`, ORG_A)).status).toBe(404); expect((yield* get(web, `/admin/users/${USER_B1}`, "Bearer org_b_key")).status).toBe(200); }), ), ); // The decision documented on the contract: the admin plane reports FOOTPRINT, // not org membership. A directory member who never touched executor is a 404, // not identity-with-empty-connections — otherwise a caller gating on this // endpoint would treat a never-onboarded invitee as onboarded. it.effect("404s an email the directory knows but who has no subject row", () => withDb((db) => Effect.gen(function* () { // Seed connections/tenant B but NOT a subject row for the directory's // only known member, so the directory resolves and storage does not. yield* touchSubject(db.db, { tenant: TENANT_B, externalId: USER_B1 }); const web = yield* singleUserWeb(db); const response = yield* get(web, `/admin/users/${encodeURIComponent(A1_EMAIL)}`, ORG_A); expect(response.status, "known to the directory, unknown to this tenant").toBe(404); expect(yield* Effect.promise(() => response.text())).toContain("AdminUserNotFound"); }), ), ); it.effect("filters the bulk lists by email, and returns an empty list for an unknown one", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* singleUserWeb(db); const filtered = yield* jsonOf( yield* get( web, `/admin/users?email=${encodeURIComponent(A1_EMAIL.toUpperCase())}`, ORG_A, ), ); expect(filtered.users.map((user) => user.externalId)).toEqual([USER_A1]); const joined = yield* jsonOf( yield* get( web, `/admin/users/with-connections?email=${encodeURIComponent(A1_EMAIL)}`, ORG_A, ), ); expect(joined.users.map((user) => user.externalId)).toEqual([USER_A1]); expect(joined.users[0]?.connections.map((c) => c.integration)).toEqual(["github"]); // An unknown email is an empty page, never an unfiltered one — the // failure mode that would quietly hand back the whole tenant. const unknown = yield* jsonOf( yield* get(web, "/admin/users?email=nobody%40users.test", ORG_A), ); expect(unknown.users).toEqual([]); }), ), ); // ── ?search= ────────────────────────────────────────────────────────────── it.effect("filters the bulk lists by a name or email substring, case-insensitively", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const searched: string[] = []; const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize, { ...stubUserDirectory({ searched }), }), ); // Part of the address, typed in the wrong case and with stray spaces: // the handler normalizes it before the directory sees it. const byEmail = yield* jsonOf( yield* get(web, `/admin/users?search=${encodeURIComponent(" A1@USERS ")}`, ORG_A), ); expect(byEmail.users.map((user) => user.externalId)).toEqual([USER_A1]); expect(byEmail.users[0]?.email, "the page still carries identity").toBe(A1_EMAIL_STORED); // Part of the name, on the joined view. const byName = yield* jsonOf( yield* get(web, "/admin/users/with-connections?search=User%20a1", ORG_A), ); expect(byName.users.map((user) => user.externalId)).toEqual([USER_A1]); expect(byName.users[0]?.connections.map((c) => c.integration)).toEqual(["github"]); // No match is an empty page, never the unfiltered tenant. const nobody = yield* jsonOf( yield* get(web, "/admin/users?search=nobody", ORG_A), ); expect(nobody.users).toEqual([]); expect(searched, "one directory search per request, normalized").toEqual([ "a1@users", "user a1", "nobody", ]); }), ), ); it.effect("a blank search is no filter at all", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const searched: string[] = []; const web = yield* webHandlerFor( stubProvider( (tenant) => platformExecutorFor(db, tenant), headerAuthorize, stubUserDirectory({ searched }), ), ); const body = yield* jsonOf(yield* get(web, "/admin/users?search=%20%20", ORG_A)); expect(body.users.map((user) => user.externalId)).toEqual([USER_A1, USER_A2]); expect(searched, "the directory is never asked to match whitespace").toEqual([]); }), ), ); it.effect("returns an empty page for a search no host directory can answer", () => withDb((db) => Effect.gen(function* () { yield* seed(db); // A directory with identities only: it cannot search, so a search // filter must select nothing rather than hand back the whole tenant. const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize, { identities: stubDirectory([]), }), ); const body = yield* jsonOf(yield* get(web, "/admin/users?search=a1", ORG_A)); expect(body.users).toEqual([]); }), ), ); it.effect("500s when the directory search fails, rather than reporting no match", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize, { search: () => Effect.fail(new DirectoryUnavailable({ message: "down" })), }), ); expect((yield* get(web, "/admin/users?search=a1", ORG_A)).status).toBe(500); }), ), ); // A resolver OUTAGE must not read as "no such user": that is a wrong answer an // operator would act on. Contrast with the identity join, which degrades to // unnamed rows precisely because it is decoration. it.effect("500s when the email resolver fails, rather than reporting no match", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* webHandlerFor( stubProvider((tenant) => platformExecutorFor(db, tenant), headerAuthorize, { resolveEmail: () => Effect.fail(new DirectoryUnavailable({ message: "down" })), }), ); expect( (yield* get(web, `/admin/users/${encodeURIComponent(A1_EMAIL)}`, ORG_A)).status, ).toBe(500); expect((yield* get(web, `/admin/users?email=${A1_EMAIL}`, ORG_A)).status).toBe(500); }), ), ); // `externalId` is opaque by contract, so the "@" test is a routing hint and // not a parse: an id that happens to contain "@" must still be findable. it.effect("falls back to a literal id lookup for an id containing @", () => withDb((db) => Effect.gen(function* () { const oddId = "user@odd"; yield* touchSubject(db.db, { tenant: TENANT_A, externalId: oddId }); const web = yield* singleUserWeb(db); const response = yield* get(web, `/admin/users/${encodeURIComponent(oddId)}`, ORG_A); expect(response.status).toBe(200); expect((yield* jsonOf(response)).user.externalId).toBe(oddId); }), ), ); it.effect("still resolves /with-connections as the list, not as a user id", () => withDb((db) => Effect.gen(function* () { yield* seed(db); const web = yield* singleUserWeb(db); // The router-ordering invariant, asserted rather than trusted: the // static segment must keep winning now that `/:identifier` exists. const body = yield* jsonOf( yield* get(web, "/admin/users/with-connections", ORG_A), ); expect(body.users.map((user) => user.externalId)).toEqual([USER_A1, USER_A2]); }), ), ); }); // --------------------------------------------------------------------------- // `?email=` is a KEYED read, not a filtered scan. // // These drive the reads directly over a recording `ExecutorAdmin`, because the // property under test is WHICH storage call the filter makes — invisible from // the HTTP edge, where a page-then-filter and a keyed read return the same // body. That equivalence is exactly what let the joined view read 100 subjects // (and their connections) to answer with one row. // --------------------------------------------------------------------------- const A_SUBJECT: AdminSubject = { externalId: USER_A1, createdAt: new Date(0), lastSeenAt: null, status: null, }; /** An `ExecutorAdmin` that answers everything and records the reads it was * asked for, so a test can assert the call the filter chose. */ const recordingAdmin = (calls: string[]): ExecutorAdmin => ({ listSubjects: (options) => { calls.push(`listSubjects:${options?.externalIds?.join(",") ?? "*"}`); return Effect.succeed([A_SUBJECT]); }, getSubject: () => { calls.push("getSubject"); return Effect.succeed(A_SUBJECT); }, listSubjectConnections: () => { calls.push("listSubjectConnections"); return Effect.succeed([]); }, listSubjectsWithConnections: (options) => { calls.push(`listSubjectsWithConnections:${options?.externalIds?.join(",") ?? "*"}`); return Effect.succeed([{ ...A_SUBJECT, connections: [] }]); }, getSubjectWithConnections: () => { calls.push("getSubjectWithConnections"); return Effect.succeed({ ...A_SUBJECT, connections: [] }); }, }); describe("admin users reads — the ?email= filter is applied before the read", () => { it.effect("resolves the joined view through the keyed read, never a page scan", () => Effect.gen(function* () { const calls: string[] = []; const body = yield* listUsersWithConnections( recordingAdmin(calls), { email: A1_EMAIL }, stubUserDirectory({}), ); expect(calls).toEqual(["getSubjectWithConnections"]); expect(body.users.map((user) => user.externalId)).toEqual([USER_A1]); }), ); it.effect("resolves the plain list through the keyed read too", () => Effect.gen(function* () { const calls: string[] = []; const body = yield* listUsers( recordingAdmin(calls), { email: A1_EMAIL }, stubUserDirectory({}), ); expect(calls).toEqual(["getSubject"]); expect(body.users.map((user) => user.externalId)).toEqual([USER_A1]); }), ); it.effect("issues NO storage read at all for an email the directory cannot name", () => Effect.gen(function* () { const calls: string[] = []; const body = yield* listUsersWithConnections( recordingAdmin(calls), { email: "nobody@users.test" }, stubUserDirectory({}), ); expect(calls).toEqual([]); expect(body.users).toEqual([]); }), ); it.effect("still pages the filtered result: offset past the only row is empty", () => Effect.gen(function* () { const calls: string[] = []; const admin = recordingAdmin(calls); const first = yield* listUsersWithConnections( admin, { email: A1_EMAIL, offset: 0, limit: 10 }, stubUserDirectory({}), ); const past = yield* listUsersWithConnections( admin, { email: A1_EMAIL, offset: 1, limit: 10 }, stubUserDirectory({}), ); expect(first.users.map((user) => user.externalId)).toEqual([USER_A1]); expect(past.users).toEqual([]); }), ); it.effect("leaves the unfiltered list on the paged read", () => Effect.gen(function* () { const calls: string[] = []; yield* listUsersWithConnections(recordingAdmin(calls), { limit: 50 }, stubUserDirectory({})); expect(calls).toEqual(["listSubjectsWithConnections:*"]); }), ); }); // --------------------------------------------------------------------------- // `?search=` is FILTER-THEN-PAGE through storage: the directory names the // matching principals, and the paged read carries exactly that set as its // `externalIds` filter — never a page scan that is filtered afterwards. // --------------------------------------------------------------------------- describe("admin users reads — the ?search= filter pages the directory's matches", () => { it.effect("hands the matched ids to the paged read, on both views", () => Effect.gen(function* () { const calls: string[] = []; const admin = recordingAdmin(calls); yield* listUsers(admin, { search: "a1" }, stubUserDirectory({})); yield* listUsersWithConnections(admin, { search: "user", limit: 10 }, stubUserDirectory({})); expect(calls).toEqual([`listSubjects:${USER_A1}`, `listSubjectsWithConnections:${USER_A1}`]); }), ); it.effect("issues NO storage read when the directory matches nobody", () => Effect.gen(function* () { const calls: string[] = []; const body = yield* listUsersWithConnections( recordingAdmin(calls), { search: "nobody" }, stubUserDirectory({}), ); expect(calls).toEqual([]); expect(body.users).toEqual([]); }), ); it.effect("lets an exact email win over a search term", () => Effect.gen(function* () { const calls: string[] = []; const searched: string[] = []; yield* listUsers( recordingAdmin(calls), { email: A1_EMAIL, search: "anything" }, stubUserDirectory({ searched }), ); expect(calls, "the keyed read, not a search").toEqual(["getSubject"]); expect(searched).toEqual([]); }), ); });