136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { OrderType, PermissionKey, SystemRole } from "@capakraken/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { scenarioRouter } from "../router/scenario.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
vi.mock("../lib/resource-capacity.js", () => ({
|
|
calculateEffectiveAvailableHours: vi.fn(),
|
|
calculateEffectiveBookedHours: vi.fn(),
|
|
loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()),
|
|
}));
|
|
|
|
const createCaller = createCallerFactory(scenarioRouter);
|
|
|
|
function createAuthenticatedCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
function createProtectedCallerWithOverrides(
|
|
db: Record<string, unknown>,
|
|
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
|
|
) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
permissionOverrides: overrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("scenario router authorization", () => {
|
|
it("requires planning read access for getProjectBaseline", async () => {
|
|
const caller = createAuthenticatedCaller({});
|
|
|
|
await expect(caller.getProjectBaseline({
|
|
projectId: "project_1",
|
|
})).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
});
|
|
|
|
it("does not treat viewCosts as a substitute for viewPlanning on getProjectBaseline", async () => {
|
|
const caller = createProtectedCallerWithOverrides({}, {
|
|
granted: [PermissionKey.VIEW_COSTS],
|
|
});
|
|
|
|
await expect(caller.getProjectBaseline({
|
|
projectId: "project_1",
|
|
})).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Planning read access required",
|
|
});
|
|
});
|
|
|
|
it("requires viewCosts for getProjectBaseline", async () => {
|
|
const caller = createProtectedCallerWithOverrides({}, {
|
|
granted: [PermissionKey.VIEW_PLANNING],
|
|
});
|
|
|
|
await expect(caller.getProjectBaseline({
|
|
projectId: "project_1",
|
|
})).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: `Permission required: ${PermissionKey.VIEW_COSTS}`,
|
|
});
|
|
});
|
|
|
|
it("allows getProjectBaseline with both planning and cost permissions", async () => {
|
|
const project = {
|
|
id: "project_1",
|
|
name: "Project One",
|
|
shortCode: "PRJ-001",
|
|
startDate: new Date("2026-04-01T00:00:00.000Z"),
|
|
endDate: new Date("2026-04-30T00:00:00.000Z"),
|
|
budgetCents: 125_000_00,
|
|
orderType: OrderType.CHARGEABLE,
|
|
};
|
|
const db = {
|
|
project: {
|
|
findUnique: vi.fn().mockResolvedValue(project),
|
|
},
|
|
assignment: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
demandRequirement: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
},
|
|
};
|
|
|
|
const caller = createProtectedCallerWithOverrides(db, {
|
|
granted: [PermissionKey.VIEW_PLANNING, PermissionKey.VIEW_COSTS],
|
|
});
|
|
const result = await caller.getProjectBaseline({
|
|
projectId: "project_1",
|
|
});
|
|
|
|
expect(db.project.findUnique).toHaveBeenCalledWith({
|
|
where: { id: "project_1" },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
shortCode: true,
|
|
startDate: true,
|
|
endDate: true,
|
|
budgetCents: true,
|
|
orderType: true,
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
project,
|
|
assignments: [],
|
|
demands: [],
|
|
totalCostCents: 0,
|
|
totalHours: 0,
|
|
budgetCents: project.budgetCents,
|
|
});
|
|
});
|
|
});
|