From fd2c6b62034cfd96a757d5d404598937c3edc670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 23:54:54 +0200 Subject: [PATCH] test(api): cover assistant webhook tools --- ...ools-import-dispo-webhooks-test-helpers.ts | 79 ++++++++++ .../assistant-tools-webhooks-errors.test.ts | 141 ++++++++++++++++++ .../assistant-tools-webhooks-read.test.ts | 61 ++++++++ 3 files changed, 281 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-import-dispo-webhooks-test-helpers.ts create mode 100644 packages/api/src/__tests__/assistant-tools-webhooks-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-webhooks-read.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-import-dispo-webhooks-test-helpers.ts b/packages/api/src/__tests__/assistant-tools-import-dispo-webhooks-test-helpers.ts new file mode 100644 index 0000000..63581bf --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-import-dispo-webhooks-test-helpers.ts @@ -0,0 +1,79 @@ +import { PermissionKey, SystemRole } from "@capakraken/shared"; +import { vi } from "vitest"; +import { apiRateLimiter } from "../middleware/rate-limit.js"; + +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(), + }; +}); + +vi.mock("../lib/cache.js", () => ({ + cacheGet: vi.fn().mockResolvedValue(null), + cacheSet: vi.fn().mockResolvedValue(undefined), + cacheInvalidate: vi.fn().mockResolvedValue(undefined), + invalidateDashboardCache: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool as executeAssistantTool, type ToolContext } from "../router/assistant-tools.js"; + +export const executeTool = executeAssistantTool; + +export function createToolContext( + db: Record, + options?: { + permissions?: PermissionKey[]; + userRole?: SystemRole; + }, +): ToolContext { + const userRole = options?.userRole ?? SystemRole.ADMIN; + return { + db: db as ToolContext["db"], + userId: "user_1", + userRole, + permissions: new Set(options?.permissions ?? []), + session: { + user: { email: "assistant@example.com", name: "Assistant User", image: null }, + expires: "2026-03-29T00:00:00.000Z", + }, + dbUser: { + id: "user_1", + systemRole: userRole, + permissionOverrides: null, + }, + roleDefaults: null, + }; +} + +export async function resetAssistantImportToolTestState() { + vi.clearAllMocks(); + vi.unstubAllEnvs(); + await apiRateLimiter.reset(); +} diff --git a/packages/api/src/__tests__/assistant-tools-webhooks-errors.test.ts b/packages/api/src/__tests__/assistant-tools-webhooks-errors.test.ts new file mode 100644 index 0000000..4b51de8 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-webhooks-errors.test.ts @@ -0,0 +1,141 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, + resetAssistantImportToolTestState, +} from "./assistant-tools-import-dispo-webhooks-test-helpers.js"; + +describe("assistant webhook tools - errors", () => { + beforeEach(async () => { + await resetAssistantImportToolTestState(); + }); + + it("returns stable assistant errors for missing webhooks", async () => { + const commands = [ + ["get_webhook", { id: "wh_missing" }], + ["update_webhook", { id: "wh_missing", data: { name: "Renamed" } }], + ["delete_webhook", { id: "wh_missing" }], + ["test_webhook", { id: "wh_missing" }], + ] as const; + + for (const [toolName, payload] of commands) { + const ctx = createToolContext( + { + webhook: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool(toolName, JSON.stringify(payload), ctx); + + expect(JSON.parse(result.content)).toEqual({ + error: "Webhook not found with the given criteria.", + }); + } + }); + + it("returns a stable assistant error for invalid webhook creation input", async () => { + const ctx = createToolContext( + { + webhook: { + create: vi.fn(), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "create_webhook", + JSON.stringify({ + name: "Primary", + url: "not-a-url", + events: ["project.updated"], + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Webhook input is invalid.", + }); + expect(ctx.db.webhook.create).not.toHaveBeenCalled(); + }); + + it("returns a stable assistant error for invalid webhook update input", async () => { + const ctx = createToolContext( + { + webhook: { + findUnique: vi.fn().mockResolvedValue({ + id: "wh_1", + name: "Primary", + url: "https://example.com/hook", + secret: null, + events: ["project.updated"], + isActive: true, + createdAt: new Date("2026-03-29T00:00:00.000Z"), + updatedAt: new Date("2026-03-29T00:00:00.000Z"), + }), + update: vi.fn(), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "update_webhook", + JSON.stringify({ + id: "wh_1", + data: { + url: "not-a-url", + }, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Webhook update input is invalid.", + }); + expect(ctx.db.webhook.update).not.toHaveBeenCalled(); + }); + + it("returns a stable assistant error when a webhook disappears during update", async () => { + const ctx = createToolContext( + { + webhook: { + findUnique: vi.fn().mockResolvedValue({ + id: "wh_1", + name: "Primary", + url: "https://example.com/hook", + secret: null, + events: ["project.updated"], + isActive: true, + createdAt: new Date("2026-03-29T00:00:00.000Z"), + updatedAt: new Date("2026-03-29T00:00:00.000Z"), + }), + update: vi.fn().mockRejectedValue({ + code: "P2025", + message: "Record to update not found.", + }), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const result = await executeTool( + "update_webhook", + JSON.stringify({ + id: "wh_1", + data: { + name: "Renamed", + }, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Webhook not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-webhooks-read.test.ts b/packages/api/src/__tests__/assistant-tools-webhooks-read.test.ts new file mode 100644 index 0000000..9c6a404 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-webhooks-read.test.ts @@ -0,0 +1,61 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { SystemRole } from "@capakraken/shared"; +import { + createToolContext, + executeTool, + resetAssistantImportToolTestState, +} from "./assistant-tools-import-dispo-webhooks-test-helpers.js"; + +describe("assistant webhook tools - read", () => { + beforeEach(async () => { + await resetAssistantImportToolTestState(); + }); + + it("masks webhook secrets in assistant responses", async () => { + const ctx = createToolContext( + { + webhook: { + findMany: vi.fn().mockResolvedValue([ + { + id: "wh_1", + name: "Primary", + url: "https://example.com/hook", + secret: "super-secret", + events: ["project.updated"], + isActive: true, + createdAt: new Date("2026-03-29T00:00:00.000Z"), + }, + ]), + findUnique: vi.fn().mockResolvedValue({ + id: "wh_1", + name: "Primary", + url: "https://example.com/hook", + secret: "super-secret", + events: ["project.updated"], + isActive: true, + createdAt: new Date("2026-03-29T00:00:00.000Z"), + }), + }, + }, + { userRole: SystemRole.ADMIN }, + ); + + const listResult = await executeTool("list_webhooks", "{}", ctx); + const getResult = await executeTool("get_webhook", JSON.stringify({ id: "wh_1" }), ctx); + + expect(JSON.parse(listResult.content)).toEqual([ + expect.objectContaining({ + id: "wh_1", + hasSecret: true, + }), + ]); + expect(JSON.parse(listResult.content)[0]).not.toHaveProperty("secret"); + expect(JSON.parse(getResult.content)).toEqual( + expect.objectContaining({ + id: "wh_1", + hasSecret: true, + }), + ); + expect(JSON.parse(getResult.content)).not.toHaveProperty("secret"); + }); +});