From 734e1eff42622c359003c44deca2802b7c87fdb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:24:21 +0200 Subject: [PATCH] test(api): cover assistant audit reads --- ...sistant-tools-audit-entity-summary.test.ts | 142 ++++++++++++++++++ .../assistant-tools-audit-errors-auth.test.ts | 68 +++++++++ .../assistant-tools-audit-log-list.test.ts | 92 ++++++++++++ .../assistant-tools-audit-read.test.ts | 101 +++++++++++++ 4 files changed, 403 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-audit-entity-summary.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-audit-errors-auth.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-audit-log-list.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-audit-read.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-audit-entity-summary.test.ts b/packages/api/src/__tests__/assistant-tools-audit-entity-summary.test.ts new file mode 100644 index 0000000..37ed53b --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-audit-entity-summary.test.ts @@ -0,0 +1,142 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-audit-task-test-helpers.js"; + +describe("assistant audit entity and summary tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns an entity-scoped audit timeline through the real audit router path", async () => { + const ctx = createToolContext( + { + auditLog: { + findMany: vi.fn().mockResolvedValue([ + { + id: "audit_entity_1", + entityType: "resource", + entityId: "resource_1", + entityName: "Peter Parker", + action: "updated", + userId: "user_2", + source: "assistant", + summary: "Updated local holiday scope", + changes: { federalState: ["HH", "BY"] }, + createdAt: new Date("2026-03-30T08:00:00.000Z"), + user: { + id: "user_2", + name: "Audit User", + email: "audit@example.com", + }, + }, + ]), + }, + }, + { userRole: SystemRole.CONTROLLER }, + ); + + const result = await executeTool( + "get_entity_timeline", + JSON.stringify({ + entityType: "resource", + entityId: "resource_1", + limit: 10, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + entityType: "resource", + entityId: "resource_1", + entityName: "Peter Parker", + itemCount: 1, + items: [ + { + id: "audit_entity_1", + entityType: "resource", + entityId: "resource_1", + entityName: "Peter Parker", + action: "updated", + userId: "user_2", + source: "assistant", + summary: "Updated local holiday scope", + createdAt: "2026-03-30T08:00:00.000Z", + changes: { federalState: ["HH", "BY"] }, + user: { + id: "user_2", + name: "Audit User", + email: "audit@example.com", + }, + }, + ], + }); + }); + + it("returns audit activity summary buckets through the real audit router path", async () => { + const ctx = createToolContext( + { + auditLog: { + groupBy: vi + .fn() + .mockResolvedValueOnce([ + { entityType: "Project", _count: { id: 3 } }, + { entityType: "Resource", _count: { id: 2 } }, + ]) + .mockResolvedValueOnce([ + { action: "CREATE", _count: { id: 2 } }, + { action: "UPDATE", _count: { id: 3 } }, + ]) + .mockResolvedValueOnce([ + { userId: "user_1", _count: { id: 4 } }, + { userId: "user_2", _count: { id: 1 } }, + ]), + count: vi.fn().mockResolvedValue(5), + }, + user: { + findMany: vi.fn().mockResolvedValue([ + { id: "user_1", name: "Larissa", email: "larissa@example.com" }, + { id: "user_2", name: null, email: "audit@example.com" }, + ]), + }, + }, + { userRole: SystemRole.CONTROLLER }, + ); + + const result = await executeTool( + "get_audit_activity_summary", + JSON.stringify({ + startDate: "2026-03-01", + endDate: "2026-03-31", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + byEntityType: { + Project: 3, + Resource: 2, + }, + byAction: { + CREATE: 2, + UPDATE: 3, + }, + byUser: [ + { name: "Larissa", count: 4 }, + { name: "audit@example.com", count: 1 }, + ], + total: 5, + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-audit-errors-auth.test.ts b/packages/api/src/__tests__/assistant-tools-audit-errors-auth.test.ts new file mode 100644 index 0000000..37f01c5 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-audit-errors-auth.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; +import { TRPCError } from "@trpc/server"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-audit-task-test-helpers.js"; + +describe("assistant audit error and access guards", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns stable assistant errors for missing audit log entries", async () => { + const ctx = createToolContext( + { + auditLog: { + findUniqueOrThrow: vi.fn().mockRejectedValue( + new TRPCError({ code: "NOT_FOUND", message: "Audit log entry not found" }), + ), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "get_audit_log_entry", + JSON.stringify({ id: "audit_missing" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Audit log entry not found with the given criteria.", + }); + }); + + it("enforces controller access for audit tools via the backing router", async () => { + const ctx = createToolContext( + { + auditLog: { + findMany: vi.fn(), + }, + }, + { userRole: SystemRole.USER }, + ); + + const result = await executeTool( + "query_change_history", + JSON.stringify({ entityType: "Project" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "You do not have permission to perform this action.", + }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-audit-log-list.test.ts b/packages/api/src/__tests__/assistant-tools-audit-log-list.test.ts new file mode 100644 index 0000000..fef5ef2 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-audit-log-list.test.ts @@ -0,0 +1,92 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-audit-task-test-helpers.js"; + +describe("assistant audit log list tool", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("lists audit entries through the real audit router path", async () => { + const ctx = createToolContext( + { + auditLog: { + findMany: vi.fn().mockResolvedValue([ + { + id: "audit_1", + entityType: "Project", + entityId: "project_1", + entityName: "Gelddruckmaschine", + action: "UPDATE", + userId: "user_1", + source: "ui", + summary: "Updated project dates", + createdAt: new Date("2026-03-28T10:00:00.000Z"), + user: { + id: "user_1", + name: "Larissa", + email: "larissa@example.com", + }, + }, + ]), + }, + }, + { userRole: SystemRole.CONTROLLER }, + ); + + const result = await executeTool( + "list_audit_log_entries", + JSON.stringify({ + entityType: "Project", + search: "Gelddruckmaschine", + limit: 10, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + filters: { + entityType: "Project", + entityId: null, + userId: null, + action: null, + source: null, + startDate: null, + endDate: null, + search: "Gelddruckmaschine", + }, + itemCount: 1, + nextCursor: null, + items: [ + { + id: "audit_1", + entityType: "Project", + entityId: "project_1", + entityName: "Gelddruckmaschine", + action: "UPDATE", + userId: "user_1", + source: "ui", + summary: "Updated project dates", + createdAt: "2026-03-28T10:00:00.000Z", + user: { + id: "user_1", + name: "Larissa", + email: "larissa@example.com", + }, + }, + ], + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-audit-read.test.ts b/packages/api/src/__tests__/assistant-tools-audit-read.test.ts new file mode 100644 index 0000000..6b84556 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-audit-read.test.ts @@ -0,0 +1,101 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + approveEstimateVersion: vi.fn(), + cloneEstimate: vi.fn(), + commitDispoImportBatch: vi.fn(), + countPlanningEntries: vi.fn().mockResolvedValue({ countsByRoleId: new Map() }), + createEstimateExport: vi.fn(), + createEstimatePlanningHandoff: vi.fn(), + createEstimateRevision: vi.fn(), + assessDispoImportReadiness: vi.fn(), + loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()), + getDashboardDemand: vi.fn().mockResolvedValue([]), + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + getDashboardOverview: vi.fn(), + getDashboardSkillGapSummary: vi.fn().mockResolvedValue({ + roleGaps: [], + totalOpenPositions: 0, + skillSupplyTop10: [], + resourcesByRole: [], + }), + getDashboardProjectHealth: vi.fn().mockResolvedValue([]), + getDashboardPeakTimes: vi.fn().mockResolvedValue([]), + getDashboardTopValueResources: vi.fn().mockResolvedValue([]), + getEstimateById: vi.fn(), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + stageDispoImportBatch: vi.fn(), + submitEstimateVersion: vi.fn(), + updateEstimateDraft: vi.fn(), + }; +}); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-audit-task-test-helpers.js"; + +describe("assistant audit read tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("routes audit timeline reads through the real audit router detail path", async () => { + const ctx = createToolContext( + { + auditLog: { + findMany: vi.fn().mockResolvedValue([ + { + id: "audit_1", + entityType: "project", + entityId: "project_1", + entityName: "Apollo", + action: "updated", + userId: "user_1", + source: "ui", + summary: "Changed budget", + changes: { budget: [1000, 1200] }, + createdAt: new Date("2026-03-29T12:00:00.000Z"), + user: { + id: "user_1", + name: "Assistant User", + email: "assistant@example.com", + }, + }, + ]), + }, + }, + { userRole: SystemRole.CONTROLLER }, + ); + + const result = await executeTool( + "get_audit_log_timeline", + JSON.stringify({ limit: 10 }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + "2026-03-29": [ + { + id: "audit_1", + entityType: "project", + entityId: "project_1", + entityName: "Apollo", + action: "updated", + userId: "user_1", + source: "ui", + summary: "Changed budget", + createdAt: "2026-03-29T12:00:00.000Z", + changes: { budget: [1000, 1200] }, + user: { + id: "user_1", + name: "Assistant User", + email: "assistant@example.com", + }, + }, + ], + }); + }); +});