Files
NextChair/src/features/booking/__tests__/authorization.test.ts
T
2026-09-05 09:32:05 +01:00

105 lines
4.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { PermissionDeniedError, assertCanAccessAppointment, canAccessAppointment } from "../authorization";
/**
* The registered-customer branch never touches the database (it's a plain
* userId comparison), so it's tested here in isolation. The guest-token
* branch's real hash/expiry/revocation behaviour is covered against a real
* database in customer-booking.integration.test.ts — this file only checks
* that authorization.ts calls into validateGuestAccessToken correctly and
* respects its result.
*/
vi.mock("@/services/scheduling/guest-access.service", () => ({
validateGuestAccessToken: vi.fn(),
}));
const { validateGuestAccessToken } = await import("@/services/scheduling/guest-access.service");
const mockValidate = vi.mocked(validateGuestAccessToken);
beforeEach(() => {
mockValidate.mockClear();
});
// Not a real PrismaClient — the mocked guest-access service never touches it.
const prisma = {} as never;
describe("assertCanAccessAppointment", () => {
it("allows a registered customer to access their own appointment", async () => {
await expect(
assertCanAccessAppointment(prisma, { id: "a1", customer: { userId: "user_1" } }, { sessionUserId: "user_1" }),
).resolves.toBeUndefined();
});
it("denies a different signed-in user from accessing a registered customer's appointment", async () => {
await expect(
assertCanAccessAppointment(prisma, { id: "a1", customer: { userId: "user_1" } }, { sessionUserId: "user_2" }),
).rejects.toThrow(PermissionDeniedError);
});
it("denies an unauthenticated caller from accessing a registered customer's appointment", async () => {
await expect(
assertCanAccessAppointment(prisma, { id: "a1", customer: { userId: "user_1" } }, { sessionUserId: undefined }),
).rejects.toThrow(PermissionDeniedError);
});
it("never even calls the guest-token validator for a registered customer's appointment", async () => {
mockValidate.mockClear();
await assertCanAccessAppointment(
prisma,
{ id: "a1", customer: { userId: "user_1" } },
{ sessionUserId: "user_1" },
).catch(() => {});
expect(mockValidate).not.toHaveBeenCalled();
});
it("allows a guest appointment when the guest-token validator confirms the token", async () => {
mockValidate.mockResolvedValueOnce(true);
await expect(
assertCanAccessAppointment(
prisma,
{ id: "a1", customer: { userId: null } },
{ sessionUserId: undefined, guestToken: "correct-token" },
),
).resolves.toBeUndefined();
expect(mockValidate).toHaveBeenCalledWith(prisma, { appointmentId: "a1", rawToken: "correct-token" });
});
it("denies a guest appointment when no token is supplied at all — the appointment id is no longer sufficient", async () => {
await expect(
assertCanAccessAppointment(prisma, { id: "a1", customer: { userId: null } }, { sessionUserId: undefined }),
).rejects.toThrow(PermissionDeniedError);
expect(mockValidate).not.toHaveBeenCalled();
});
it("denies a guest appointment when the guest-token validator rejects the token", async () => {
mockValidate.mockResolvedValueOnce(false);
await expect(
assertCanAccessAppointment(
prisma,
{ id: "a1", customer: { userId: null } },
{ sessionUserId: undefined, guestToken: "wrong-token" },
),
).rejects.toThrow(PermissionDeniedError);
});
});
describe("canAccessAppointment", () => {
it("mirrors assertCanAccessAppointment as a boolean", async () => {
expect(
await canAccessAppointment(prisma, { id: "a1", customer: { userId: "user_1" } }, { sessionUserId: "user_1" }),
).toBe(true);
expect(
await canAccessAppointment(prisma, { id: "a1", customer: { userId: "user_1" } }, { sessionUserId: "user_2" }),
).toBe(false);
mockValidate.mockResolvedValueOnce(true);
expect(
await canAccessAppointment(
prisma,
{ id: "a1", customer: { userId: null } },
{ sessionUserId: undefined, guestToken: "correct-token" },
),
).toBe(true);
});
});