Extract groombook/api from monorepo with CI workflow
- 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>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user