import { describe, expect, it, vi } from "vitest"; import type { Db } from "../db/client.js"; import { IdentityConflictError, upsertUserFromIdentity } from "./upsert-user.js"; function createDb(options: { bySub?: Record | null; byEmail?: Record | null; }): { handle: Db; setSpy: ReturnType } { const findFirst = vi.fn(async (_args: { where: unknown }) => { // drizzle eq objects aren't introspectable here; alternate by call order. if (findFirst.mock.calls.length === 1) { return options.bySub ?? null; } return options.byEmail ?? null; }); const returningRow = { id: "11111111-1111-4111-8111-111111111111", cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", role: "admin" as const, }; const setSpy = vi.fn((patch: Record) => ({ where: vi.fn(() => ({ returning: vi.fn(async () => [ { ...returningRow, ...patch, role: patch.role ?? returningRow.role }, ]), })), })); return { handle: { driver: "postgres", pool: { end: vi.fn(async () => undefined) } as never, db: { query: { users: { findFirst } }, update: vi.fn(() => ({ set: setSpy, })), insert: vi.fn(() => ({ values: vi.fn(() => ({ returning: vi.fn(async () => [returningRow]), })), })), } as never, }, setSpy, }; } describe("upsertUserFromIdentity", () => { it("updates an existing row matched by cognito sub", async () => { const { handle } = createDb({ bySub: { id: "11111111-1111-4111-8111-111111111111", cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Old Name", role: "admin", }, }); const user = await upsertUserFromIdentity(handle, { cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", role: "admin", }); expect(user.cognitoSub).toBe("seed-sub-admin"); expect(handle.db.update).toHaveBeenCalled(); }); it("preserves the stored role when the token omits a role claim", async () => { const { handle, setSpy } = createDb({ bySub: { id: "11111111-1111-4111-8111-111111111111", cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", role: "admin", }, }); const user = await upsertUserFromIdentity(handle, { cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", }); expect(setSpy).toHaveBeenCalledWith(expect.not.objectContaining({ role: expect.anything() })); expect(user.role).toBe("admin"); }); it("writes an explicit role claim over the stored role", async () => { const { handle, setSpy } = createDb({ bySub: { id: "11111111-1111-4111-8111-111111111111", cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", role: "viewer", }, }); await upsertUserFromIdentity(handle, { cognitoSub: "seed-sub-admin", email: "admin@seahavenind.com", name: "Dev Admin", role: "approver", }); expect(setSpy).toHaveBeenCalledWith(expect.objectContaining({ role: "approver" })); }); it("refuses to rebind an email owned by a different cognito sub", async () => { const { handle } = createDb({ bySub: null, byEmail: { id: "11111111-1111-4111-8111-111111111111", cognitoSub: "other-sub", email: "admin@seahavenind.com", name: "Seed Admin", role: "admin", }, }); await expect( upsertUserFromIdentity(handle, { cognitoSub: "attacker-sub", email: "admin@seahavenind.com", name: "Attacker", role: "admin", }), ).rejects.toBeInstanceOf(IdentityConflictError); }); });