feat(estimate): scope estimate search to controller audience
This commit is contained in:
@@ -530,23 +530,28 @@ describe("assistant router tool gating", () => {
|
||||
const managerWithoutCosts = getToolNames([], SystemRole.MANAGER);
|
||||
const userNames = getToolNames([PermissionKey.VIEW_COSTS], SystemRole.USER);
|
||||
|
||||
expect(controllerNames).toContain("search_estimates");
|
||||
expect(controllerNames).toContain("get_estimate_detail");
|
||||
expect(controllerNames).toContain("list_estimate_versions");
|
||||
expect(controllerNames).toContain("get_estimate_version_snapshot");
|
||||
expect(controllerNames).toContain("get_estimate_weekly_phasing");
|
||||
expect(controllerNames).toContain("get_estimate_commercial_terms");
|
||||
expect(controllerWithoutCosts).toContain("search_estimates");
|
||||
expect(controllerWithoutCosts).not.toContain("get_estimate_detail");
|
||||
expect(controllerWithoutCosts).toContain("list_estimate_versions");
|
||||
expect(controllerWithoutCosts).not.toContain("get_estimate_version_snapshot");
|
||||
expect(controllerWithoutCosts).toContain("get_estimate_weekly_phasing");
|
||||
expect(controllerWithoutCosts).toContain("get_estimate_commercial_terms");
|
||||
expect(managerNames).toContain("search_estimates");
|
||||
expect(managerNames).toContain("get_estimate_detail");
|
||||
expect(managerNames).toContain("list_estimate_versions");
|
||||
expect(managerNames).toContain("get_estimate_version_snapshot");
|
||||
expect(managerNames).toContain("get_estimate_weekly_phasing");
|
||||
expect(managerNames).toContain("get_estimate_commercial_terms");
|
||||
expect(managerWithoutCosts).toContain("search_estimates");
|
||||
expect(managerWithoutCosts).toContain("list_estimate_versions");
|
||||
expect(managerWithoutCosts).not.toContain("get_estimate_version_snapshot");
|
||||
expect(userNames).not.toContain("search_estimates");
|
||||
expect(userNames).not.toContain("get_estimate_detail");
|
||||
expect(userNames).not.toContain("list_estimate_versions");
|
||||
expect(userNames).not.toContain("get_estimate_version_snapshot");
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
EstimateExportFormat,
|
||||
EstimateStatus,
|
||||
EstimateVersionStatus,
|
||||
PermissionKey,
|
||||
SystemRole,
|
||||
} from "@capakraken/shared";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -62,6 +63,24 @@ function createProtectedCaller(db: Record<string, unknown>) {
|
||||
});
|
||||
}
|
||||
|
||||
function createProtectedCallerWithOverrides(
|
||||
db: Record<string, unknown>,
|
||||
overrides: { granted?: PermissionKey[]; denied?: PermissionKey[] } | null,
|
||||
) {
|
||||
return createCaller({
|
||||
session: {
|
||||
user: { email: "viewer@example.com", name: "Viewer", image: null },
|
||||
expires: "2099-01-01T00:00:00.000Z",
|
||||
},
|
||||
db: db as never,
|
||||
dbUser: {
|
||||
id: "user_3",
|
||||
systemRole: SystemRole.USER,
|
||||
permissionOverrides: overrides,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -107,11 +126,33 @@ describe("estimate router", () => {
|
||||
// ─── list ──────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("list", () => {
|
||||
it("requires controller access", async () => {
|
||||
const caller = createProtectedCaller({});
|
||||
|
||||
await expect(caller.list({})).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "FORBIDDEN",
|
||||
message: "Controller access required",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows controllers to list estimates", async () => {
|
||||
const findMany = vi.fn().mockResolvedValue([baseEstimate]);
|
||||
const db = { estimate: { findMany } };
|
||||
|
||||
const caller = createControllerCaller(db);
|
||||
const result = await caller.list({});
|
||||
|
||||
expect(findMany).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("returns all estimates without filters", async () => {
|
||||
const findMany = vi.fn().mockResolvedValue([baseEstimate]);
|
||||
const db = { estimate: { findMany } };
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const caller = createManagerCaller(db);
|
||||
const result = await caller.list({});
|
||||
|
||||
expect(findMany).toHaveBeenCalled();
|
||||
@@ -122,7 +163,7 @@ describe("estimate router", () => {
|
||||
const findMany = vi.fn().mockResolvedValue([]);
|
||||
const db = { estimate: { findMany } };
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const caller = createManagerCaller(db);
|
||||
await caller.list({ projectId: "project_1" });
|
||||
|
||||
expect(findMany).toHaveBeenCalledWith(
|
||||
@@ -136,7 +177,7 @@ describe("estimate router", () => {
|
||||
const findMany = vi.fn().mockResolvedValue([]);
|
||||
const db = { estimate: { findMany } };
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const caller = createManagerCaller(db);
|
||||
await caller.list({ status: EstimateStatus.DRAFT });
|
||||
|
||||
expect(findMany).toHaveBeenCalledWith(
|
||||
@@ -150,7 +191,7 @@ describe("estimate router", () => {
|
||||
const findMany = vi.fn().mockResolvedValue([]);
|
||||
const db = { estimate: { findMany } };
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const caller = createManagerCaller(db);
|
||||
await caller.list({ query: "search" });
|
||||
|
||||
expect(findMany).toHaveBeenCalledWith(
|
||||
@@ -161,6 +202,19 @@ describe("estimate router", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not grant estimate listing through standalone viewCosts overrides", async () => {
|
||||
const caller = createProtectedCallerWithOverrides({}, {
|
||||
granted: [PermissionKey.VIEW_COSTS],
|
||||
});
|
||||
|
||||
await expect(caller.list({})).rejects.toThrow(
|
||||
expect.objectContaining({
|
||||
code: "FORBIDDEN",
|
||||
message: "Controller access required",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getById ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user