import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; import { getAuth } from "../lib/auth.js"; const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440001"; const CLIENT_EMAIL = "alice@example.com"; const CLIENT_NAME = "Alice Smith"; const UAT_CUSTOMER_ID = "c0000001-0000-0000-0000-000000000001"; const UAT_CUSTOMER_EMAIL = "uat-customer@groombook.dev"; const UAT_CUSTOMER_NAME = "UAT Customer"; const BETTER_AUTH_SESSION = { user: { id: "auth-user-001", email: CLIENT_EMAIL, name: CLIENT_NAME, }, session: { id: "ba-session-001", expiresAt: new Date(Date.now() + 60 * 60 * 1000), }, }; const MOCK_CLIENT = { id: CLIENT_ID, email: CLIENT_EMAIL, name: CLIENT_NAME, }; let mockGetAuth: ReturnType; let mockGetSession: ReturnType; let insertedSession: Record | null = null; let mockClientRow: Record | null = null; let mockStaffRow: Record | null = null; function makeChainable(data: unknown[]): unknown { const arr = [...data]; return new Proxy(arr, { get(target, prop) { if (prop === "where" || prop === "orderBy" || prop === "limit") { return () => makeChainable(target); } // @ts-expect-error proxy return target[prop]; }, }); } vi.mock("@groombook/db", () => { const impersonationSessions = new Proxy( { _name: "impersonationSessions" }, { get: (t, p) => (p === "_name" ? "impersonationSessions" : { table: "impersonationSessions", column: p }) } ); const clients = new Proxy( { _name: "clients" }, { get: (t, p) => (p === "_name" ? "clients" : { table: "clients", column: p }) } ); const staff = new Proxy( { _name: "staff" }, { get: (t, p) => (p === "_name" ? "staff" : { table: "staff", column: p }) } ); return { getDb: () => ({ select: () => ({ from: (table: { _name: string }) => { if (table._name === "clients") { return makeChainable(mockClientRow ? [mockClientRow] : []); } if (table._name === "staff") { return makeChainable(mockStaffRow ? [mockStaffRow] : []); } return makeChainable([]); }, }), insert: (table: { _name: string }) => ({ values: (vals: Record) => ({ returning: () => { if (table._name === "impersonationSessions") { insertedSession = { id: "new-session-001", ...vals }; return [insertedSession]; } return []; }, }), }), }), impersonationSessions, clients, staff, eq: vi.fn(), and: vi.fn(), inArray: vi.fn(), }; }); vi.mock("../lib/auth.js", () => ({ getAuth: vi.fn(), })); const { portalRouter } = await import("../routes/portal.js"); const app = new Hono(); app.route("/portal", portalRouter); describe("POST /portal/session-from-auth", () => { beforeEach(() => { insertedSession = null; mockClientRow = null; mockStaffRow = null; mockGetSession = vi.fn(); mockGetAuth = vi.fn(() => ({ api: { getSession: mockGetSession, }, })); vi.mocked(getAuth).mockImplementation(mockGetAuth); }); it("returns 401 when no Better Auth session", async () => { mockGetSession.mockResolvedValue(null); const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(401); const body = await res.json(); expect(body.error).toBe("Unauthorized"); }); it("returns 404 when authenticated user has no client record", async () => { mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION); mockClientRow = null; const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(404); const body = await res.json(); expect(body.error).toBe("No client record found for this user"); }); it("returns a portal session with sessionId, clientId, clientName when client is found", async () => { mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION); mockClientRow = MOCK_CLIENT; mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" }; const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(201); const body = await res.json(); expect(body).toHaveProperty("sessionId"); expect(body).toHaveProperty("clientId", CLIENT_ID); expect(body).toHaveProperty("clientName", CLIENT_NAME); }); it("creates a portal session with reason sso-bridge", async () => { mockGetSession.mockResolvedValue(BETTER_AUTH_SESSION); mockClientRow = MOCK_CLIENT; mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" }; const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(201); expect(insertedSession).not.toBeNull(); expect((insertedSession as Record).reason).toBe("sso-bridge"); }); it("returns 201 for uat-customer SSO bridge with correct clientId and clientName", async () => { const uatAuthSession = { user: { id: "auth-user-uat-customer", email: UAT_CUSTOMER_EMAIL, name: UAT_CUSTOMER_NAME, }, session: { id: "ba-session-uat-customer", expiresAt: new Date(Date.now() + 60 * 60 * 1000), }, }; mockGetSession.mockResolvedValue(uatAuthSession); mockClientRow = { id: UAT_CUSTOMER_ID, email: UAT_CUSTOMER_EMAIL, name: UAT_CUSTOMER_NAME }; mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" }; const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(201); const body = await res.json(); expect(body).toHaveProperty("sessionId"); expect(body.clientId).toBe(UAT_CUSTOMER_ID); expect(body.clientName).toBe(UAT_CUSTOMER_NAME); expect(insertedSession).not.toBeNull(); expect((insertedSession as Record).reason).toBe("sso-bridge"); }); it("returns 503 when auth is not configured", async () => { mockGetAuth.mockImplementation(() => { throw new Error("Auth not initialized"); }); const res = await app.request("/portal/session-from-auth", { method: "POST", }); expect(res.status).toBe(503); }); });