280c699d0d
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Test (pull_request) Successful in 10s
CI / Build & Push Docker Images (pull_request) Failing after 37s
CI / Lint & Typecheck (push) Successful in 14s
CI / Test (push) Successful in 2m19s
CI / Build & Push Docker Images (push) Failing after 33s
206 lines
6.3 KiB
TypeScript
206 lines
6.3 KiB
TypeScript
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<typeof vi.fn>;
|
|
let mockGetSession: ReturnType<typeof vi.fn>;
|
|
let insertedSession: Record<string, unknown> | null = null;
|
|
let mockClientRow: Record<string, unknown> | null = null;
|
|
let mockStaffRow: Record<string, unknown> | 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<string, unknown>) => ({
|
|
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<string, unknown>).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<string, unknown>).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);
|
|
});
|
|
}); |