abac9dfe6c
- Add source code from apps/api - Add packages/db and packages/types workspace dependencies - Add GitHub Actions CI workflow (lint, typecheck, test, docker) - Generate pnpm-lock.yaml - Add .gitignore Co-Authored-By: Paperclip <noreply@paperclip.ing>
107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|