import { describe, expect, test, beforeAll, afterAll } from "bun:test"; import { Elysia } from "elysia"; import { toErrorResponse } from "@continuum/ts-core"; import { roleSatisfies, ORG_ROLES } from "../src/auth/roles"; import { startDevAuthIssuer } from "../scripts/dev-auth"; const USER = "11111111-1111-1111-1111-111111111111"; describe("the role ladder", () => { test("is ordered viewer < operator < owner", () => { expect(ORG_ROLES).toEqual(["viewer", "operator", "owner"]); }); // Written out in full rather than derived from the ladder: a table that // computes the expected answer the same way the implementation does agrees // with a wrong implementation. const cases: Array<[string, string, boolean]> = [ ["viewer", "viewer", true], ["viewer", "operator", false], ["viewer", "owner", false], ["operator", "viewer", true], ["operator", "operator", true], ["operator", "owner", false], ["owner", "viewer", true], ["owner", "operator", true], ["owner", "owner", true], ]; for (const [actual, minimum, expected] of cases) { test(`'${actual}' ${expected ? "satisfies" : "does not satisfy"} '${minimum}'`, () => { expect(roleSatisfies(actual as never, minimum as never)).toBe(expected); }); } test("a viewer cannot send printer commands", () => { // The boundary that moves physical machines, called out on its own // because it is the one that is expensive to get wrong. expect(roleSatisfies("viewer", "operator")).toBe(false); }); }); /** * Exercises the real verification path - `createRemoteJWKSet` against a real * JWKS endpoint over HTTP - rather than a stubbed verifier. The things that * actually go wrong here (an unsigned token accepted, a token signed by * another key accepted, an expired token accepted) are all invisible to a * test that mocks the verifier out. */ describe("requireUser", () => { let issuer: Awaited>; let app: Elysia; beforeAll(async () => { issuer = await startDevAuthIssuer(); process.env.SUPABASE_URL = issuer.url; // Imported after SUPABASE_URL is set: the verifier is built at module // load so a misconfigured deployment fails at boot, not on first request. const { requireUser } = await import("../src/auth/plugin"); app = new Elysia().onError(toErrorResponse).use(requireUser).get("/whoami", ({ user }) => user); }); afterAll(() => issuer.stop()); const get = (headers: Record = {}) => app.handle(new Request("http://localhost/whoami", { headers })); test("accepts a properly signed token and exposes the subject", async () => { const token = await issuer.mint({ sub: USER, email: "farm@example.test" }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ id: USER, email: "farm@example.test" }); }); test("rejects a request with no Authorization header", async () => { const response = await get(); expect(response.status).toBe(401); expect((await response.json()).error.code).toBe("UNAUTHORIZED"); }); test("rejects a non-Bearer Authorization header", async () => { const response = await get({ authorization: "Basic dXNlcjpwYXNz" }); expect(response.status).toBe(401); }); test("rejects a token signed by a key that is not in the JWKS", async () => { const token = await issuer.mintWithForeignKey({ sub: USER }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("rejects an expired token", async () => { const token = await issuer.mint({ sub: USER, expiresIn: -60 }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("rejects a token issued for another audience", async () => { // Supabase issues `aud: "authenticated"` for user sessions. Its anon and // service-role keys are also valid project JWTs with other audiences, so // without this check the public anon key would authenticate as a user. const token = await issuer.mint({ sub: USER, audience: "anon" }); const response = await get({ authorization: `Bearer ${token}` }); expect(response.status).toBe(401); }); test("does not say why a token was rejected", async () => { // Distinguishing "expired" from "bad signature" tells whoever is probing // which part of a forged token to fix next. const expired = await get({ authorization: `Bearer ${await issuer.mint({ sub: USER, expiresIn: -60 })}`, }); const foreign = await get({ authorization: `Bearer ${await issuer.mintWithForeignKey({ sub: USER })}`, }); expect((await expired.json()).error.message).toBe((await foreign.json()).error.message); }); }); describe("configuration", () => { test("the process refuses to start without SUPABASE_URL", async () => { // Fail at boot, not on the first authenticated request: a backend that // starts and then 500s on every login looks like an outage somewhere // else. Run in a subprocess because the module caches its verifier. const proc = Bun.spawn(["bun", "-e", "await import('./src/auth/verifier.ts')"], { cwd: import.meta.dir + "/..", env: { ...process.env, SUPABASE_URL: "" }, stderr: "pipe", stdout: "pipe", }); expect(await proc.exited).not.toBe(0); expect(await new Response(proc.stderr).text()).toContain("SUPABASE_URL"); }); });