/** * Groomer Isolation Tests * * Validates row-level data scoping for the groomer role. * * The role guard tests verify the core groomer identification logic. * Integration tests with the real database validate the full filter behavior. */ import { describe, it, expect } from "vitest"; import type { StaffRow } from "../middleware/rbac.js"; // ─── Mock staff ─────────────────────────────────────────────────────────────── const MANAGER: StaffRow = { id: "staff-manager-id", oidcSub: "oidc-manager-sub", userId: null, role: "manager", isSuperUser: true, name: "Manager McManager", email: "manager@example.com", active: true, icalToken: null, createdAt: new Date(), updatedAt: new Date(), }; const GROOMER: StaffRow = { ...MANAGER, id: "staff-groomer-id", oidcSub: "oidc-groomer-sub", role: "groomer", name: "Groomer Gary", email: "groomer@example.com", }; const RECEPTIONIST: StaffRow = { ...MANAGER, id: "staff-receptionist-id", oidcSub: "oidc-receptionist-sub", role: "receptionist", name: "Receptionist Rita", email: "receptionist@example.com", }; // ─── Role guard ────────────────────────────────────────────────────────────── /** * The isGroomer guard (staffRow?.role === "groomer") is the foundation of * all row-level filtering in appointments.ts, clients.ts, and pets.ts. * These tests verify it handles all roles correctly. */ describe("Groomer role guard", () => { const isGroomer = (s: StaffRow | undefined) => s?.role === "groomer"; it("manager is not groomer", () => expect(isGroomer(MANAGER)).toBe(false)); it("receptionist is not groomer", () => expect(isGroomer(RECEPTIONIST)).toBe(false)); it("groomer is groomer", () => expect(isGroomer(GROOMER)).toBe(true)); /** Safe fallback when staff context is not set (e.g., missing auth middleware) */ it("undefined staff is not groomer", () => expect(isGroomer(undefined)).toBe(false)); }); // ─── Groomer filter data shapes ─────────────────────────────────────────────── /** * These constants match the shape used in route handlers to validate * the groomer filter conditions: * or(eq(appointments.staffId, staffRow.id), eq(appointments.batherStaffId, staffRow.id)) * This verifies the groomer can see appointments they own OR bathe. */ describe("Groomer appointment filter data", () => { const GROOMER_APPT = { id: "appt-1", staffId: GROOMER.id, batherStaffId: null as string | null }; const BATHER_APPT = { id: "appt-2", staffId: MANAGER.id, batherStaffId: GROOMER.id }; const OTHER_APPT = { id: "appt-3", staffId: MANAGER.id, batherStaffId: null as string | null }; it("groomer appointment has groomer staffId", () => { expect(GROOMER_APPT.staffId).toBe(GROOMER.id); expect(GROOMER_APPT.batherStaffId).toBeNull(); }); it("groomer can see appointment where they are the bather", () => { expect(BATHER_APPT.batherStaffId).toBe(GROOMER.id); expect(BATHER_APPT.staffId).toBe(MANAGER.id); }); it("other appointment is not assigned to groomer", () => { expect(OTHER_APPT.staffId).toBe(MANAGER.id); expect(OTHER_APPT.batherStaffId).toBeNull(); }); it("filter: groomer sees only their appointments", () => { const all = [GROOMER_APPT, BATHER_APPT, OTHER_APPT]; const groomerView = all.filter( (a) => a.staffId === GROOMER.id || a.batherStaffId === GROOMER.id ); expect(groomerView).toHaveLength(2); expect(groomerView.map((a) => a.id)).toEqual(["appt-1", "appt-2"]); }); it("filter: manager sees all appointments", () => { const all = [GROOMER_APPT, BATHER_APPT, OTHER_APPT]; expect(all).toHaveLength(3); }); });