feat(estimate): scope estimate search to controller audience

This commit is contained in:
2026-03-30 09:44:50 +02:00
parent 806c028974
commit 98502e6cf8
4 changed files with 65 additions and 5 deletions
@@ -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 ───────────────────────────────────────────────────────────────