import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { hasPermission, isSuperAdmin } from "@/lib/auth/permissions"; import { assertOwnerPermission, OwnerPermissionDeniedError } from "../authorization"; import { deactivateStaff, updateStaffAsOwner } from "@/services/staff/staff.service"; import { deleteCategory } from "@/services/catalogue/service-catalogue.service"; import { Permission } from "@/generated/prisma/enums"; import { cleanupTestBusiness, createTestBusiness, createTestStaff, prisma, } from "@/services/scheduling/__tests__/fixtures"; import type { TestBusiness } from "@/services/scheduling/__tests__/fixtures"; /** * Real-DB coverage of "a normal barber must not be able to perform owner * actions" and "owner actions never leak across businesses" — the two * defense-in-depth rules the Phase 6 brief calls out repeatedly. This * re-derives permissions from the database exactly as the owner Server * Actions do (assertOwnerPermission -> hasPermission -> a fresh * StaffProfile/Role/RolePermission query), rather than trusting a cached * session claim. */ describe("Owner authorization and business isolation (integration)", () => { let business: TestBusiness; beforeEach(async () => { business = await createTestBusiness(); }); afterEach(async () => { await cleanupTestBusiness(business); }); describe("hasPermission / assertOwnerPermission", () => { it("grants MANAGE_STAFF to a staff member whose role holds it", async () => { const ownerRole = await prisma.role.create({ data: { businessId: business.businessId, name: "Owner", permissions: { create: [{ permission: "MANAGE_STAFF" }] }, }, }); const { userId } = await createTestStaff(business); await prisma.staffProfile.update({ where: { userId }, data: { roleId: ownerRole.id } }); expect(await hasPermission(userId, "MANAGE_STAFF")).toBe(true); await expect(assertOwnerPermission(userId, "MANAGE_STAFF")).resolves.toBeUndefined(); }); it("denies MANAGE_STAFF to an ordinary barber whose role doesn't hold it", async () => { const { userId } = await createTestStaff(business); // createTestStaff's auto-role has no permissions expect(await hasPermission(userId, "MANAGE_STAFF")).toBe(false); await expect(assertOwnerPermission(userId, "MANAGE_STAFF")).rejects.toBeInstanceOf(OwnerPermissionDeniedError); }); it("reflects a permission change immediately — no caching across the role edit", async () => { const barberRole = await prisma.role.create({ data: { businessId: business.businessId, name: "Barber" } }); const { userId } = await createTestStaff(business); await prisma.staffProfile.update({ where: { userId }, data: { roleId: barberRole.id } }); expect(await hasPermission(userId, "MANAGE_SERVICES")).toBe(false); await prisma.rolePermission.create({ data: { roleId: barberRole.id, permission: "MANAGE_SERVICES" } }); expect(await hasPermission(userId, "MANAGE_SERVICES")).toBe(true); }); }); describe("business isolation", () => { it("updateStaffAsOwner can't reach a staff member from a different business", async () => { const otherBusiness = await createTestBusiness(); try { const { staffId: otherStaffId } = await createTestStaff(otherBusiness); const result = await updateStaffAsOwner(prisma, { staffId: otherStaffId, businessId: business.businessId, input: { bio: "hijacked" }, }); expect(result.success).toBe(false); if (result.success) throw new Error("unreachable"); expect(result.validation.issues[0]?.code).toBe("STAFF_NOT_FOUND"); const unaffected = await prisma.staffProfile.findUniqueOrThrow({ where: { id: otherStaffId } }); expect(unaffected.bio).not.toBe("hijacked"); } finally { await cleanupTestBusiness(otherBusiness); } }); it("deactivateStaff can't reach a staff member from a different business", async () => { const otherBusiness = await createTestBusiness(); try { const { staffId: otherStaffId } = await createTestStaff(otherBusiness); const result = await deactivateStaff(prisma, { staffId: otherStaffId, businessId: business.businessId }); expect(result.success).toBe(false); const unaffected = await prisma.staffProfile.findUniqueOrThrow({ where: { id: otherStaffId } }); expect(unaffected.status).toBe("ACTIVE"); } finally { await cleanupTestBusiness(otherBusiness); } }); it("deleteCategory can't reach a category from a different business", async () => { const otherBusiness = await createTestBusiness(); try { // Move the other business's fixture category to be empty so only // business-scoping (not the has-services guard) is under test. await prisma.service.deleteMany({ where: { businessId: otherBusiness.businessId } }); const result = await deleteCategory(prisma, { categoryId: otherBusiness.categoryId, businessId: business.businessId, }); expect(result.success).toBe(false); if (result.success) throw new Error("unreachable"); expect(result.validation.issues[0]?.code).toBe("CATEGORY_NOT_FOUND"); const stillExists = await prisma.serviceCategory.findUnique({ where: { id: otherBusiness.categoryId } }); expect(stillExists).not.toBeNull(); } finally { await cleanupTestBusiness(otherBusiness); } }); }); describe("Super Administrator (Phase 9)", () => { /** Deliberately no StaffProfile at all — see scripts/ensure-super-admin.ts and docs/phase-9-summary.md. */ async function createTestSuperAdmin() { const user = await prisma.user.create({ data: { email: `super-admin-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@test.local`, isSuperAdmin: true }, }); return user.id; } it("isSuperAdmin reflects the User.isSuperAdmin flag", async () => { const { userId: ordinaryUserId } = await createTestStaff(business); const superAdminUserId = await createTestSuperAdmin(); try { expect(await isSuperAdmin(ordinaryUserId)).toBe(false); expect(await isSuperAdmin(superAdminUserId)).toBe(true); } finally { await prisma.user.delete({ where: { id: superAdminUserId } }); } }); it("hasPermission grants every permission to a Super Administrator with no StaffProfile at all", async () => { const superAdminUserId = await createTestSuperAdmin(); try { for (const permission of Object.values(Permission)) { expect(await hasPermission(superAdminUserId, permission)).toBe(true); } expect(await prisma.staffProfile.findUnique({ where: { userId: superAdminUserId } })).toBeNull(); } finally { await prisma.user.delete({ where: { id: superAdminUserId } }); } }); it("does not grant permissions to an ordinary user who merely lacks a StaffProfile", async () => { const user = await prisma.user.create({ data: { email: `plain-${Date.now()}@test.local` } }); try { expect(await hasPermission(user.id, Permission.MANAGE_STAFF)).toBe(false); } finally { await prisma.user.delete({ where: { id: user.id } }); } }); }); });