test(api): harden estimate races and user auth boundaries

This commit is contained in:
2026-03-30 12:32:51 +02:00
parent 3c4894a966
commit 019c267435
3 changed files with 192 additions and 1 deletions
@@ -2889,6 +2889,113 @@ describe("assistant import/export and dispo tools", () => {
});
});
it("returns stable assistant errors when estimate creation loses referenced records mid-write", async () => {
const cases = [
{
name: "missing resource reference",
payload: {
name: "Delivery Estimate",
demandLines: [
{
resourceId: "resource_1",
lineType: "LABOR",
name: "Animation",
hours: 40,
costRateCents: 0,
billRateCents: 0,
currency: "EUR",
costTotalCents: 0,
priceTotalCents: 0,
monthlySpread: {},
staffingAttributes: {},
metadata: {},
},
],
},
rejection: {
code: "P2003",
message: "Foreign key constraint failed",
meta: { field_name: "EstimateDemandLine_resourceId_fkey" },
},
expected: "Resource not found with the given criteria.",
},
{
name: "missing scope item reference",
payload: {
name: "Delivery Estimate",
scopeItems: [
{
sequenceNo: 1,
scopeType: "SHOT",
name: "Shot 010",
technicalSpec: {},
metadata: {},
},
],
demandLines: [
{
scopeItemId: "scope_item_missing",
lineType: "LABOR",
name: "Lighting",
hours: 24,
costRateCents: 0,
billRateCents: 0,
currency: "EUR",
costTotalCents: 0,
priceTotalCents: 0,
monthlySpread: {},
staffingAttributes: {},
metadata: {},
},
],
},
rejection: {
code: "P2003",
message: "Foreign key constraint failed",
meta: { field_name: "EstimateDemandLine_scopeItemId_fkey" },
},
expected: "Estimate scope item not found with the given criteria.",
},
{
name: "generic referenced record race",
payload: { name: "Delivery Estimate" },
rejection: {
code: "P2025",
message: "Record to create no longer references a valid row",
meta: { cause: "Dependent record disappeared during nested estimate create" },
},
expected: "One of the referenced project, role, resource, or scope items no longer exists.",
},
] as const;
for (const testCase of cases) {
const ctx = createToolContext(
{
estimate: {
create: vi.fn().mockRejectedValue(testCase.rejection),
},
rateCardLine: {
findMany: vi.fn().mockResolvedValue([]),
},
},
{
userRole: SystemRole.MANAGER,
permissions: [PermissionKey.MANAGE_PROJECTS],
},
);
const result = await executeTool(
"create_estimate",
JSON.stringify(testCase.payload),
ctx,
);
expect(JSON.parse(result.content)).toEqual({
error: testCase.expected,
});
}
});
it("returns stable assistant errors for estimate mutation tools backed by estimate application use-cases", async () => {
const cases = [
{
@@ -34,6 +34,57 @@ function createContext(
}
describe("user router authorization", () => {
it("requires authentication for self-service profile lookups", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.me()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("requires authentication for dashboard layout reads", async () => {
const findUnique = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
},
}, { session: false }));
await expect(caller.getDashboardLayout()).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
});
it("requires authentication for favorite project toggles", async () => {
const findUnique = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique,
update,
},
}, { session: false }));
await expect(caller.toggleFavoriteProject({ projectId: "project_1" })).rejects.toMatchObject({
code: "UNAUTHORIZED",
message: "Authentication required",
});
expect(findUnique).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("forbids regular users from listing assignable users", async () => {
const findMany = vi.fn();
const caller = createCaller(createContext({
@@ -105,6 +156,36 @@ describe("user router authorization", () => {
expect(findUnique).not.toHaveBeenCalled();
});
it("forbids non-admin users from linking resources", async () => {
const userFindUnique = vi.fn();
const resourceFindUnique = vi.fn();
const updateMany = vi.fn();
const update = vi.fn();
const caller = createCaller(createContext({
user: {
findUnique: userFindUnique,
},
resource: {
findUnique: resourceFindUnique,
updateMany,
update,
},
}, { role: SystemRole.MANAGER }));
await expect(caller.linkResource({
userId: "user_2",
resourceId: "resource_1",
})).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(userFindUnique).not.toHaveBeenCalled();
expect(resourceFindUnique).not.toHaveBeenCalled();
expect(updateMany).not.toHaveBeenCalled();
expect(update).not.toHaveBeenCalled();
});
it("keeps TOTP verification public for the login flow", async () => {
const findUniqueOrThrow = vi.fn().mockResolvedValue({
id: "user_1",