feat(scenario): scope baseline reads to planning and cost audiences
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user