From d7044b6053423b200d8a2a2520b197c779ff46ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 1 Apr 2026 00:01:03 +0200 Subject: [PATCH] test(api): cover assistant holiday mutations --- ...-holiday-calendar-mutations-guards.test.ts | 65 +++++++ ...holiday-calendar-mutations-success.test.ts | 176 ++++++++++++++++++ ...ols-holiday-entry-mutations-errors.test.ts | 90 +++++++++ ...ls-holiday-entry-mutations-success.test.ts | 124 ++++++++++++ ...tant-tools-holiday-mutation-errors.test.ts | 117 ++++++++++++ 5 files changed, 572 insertions(+) create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-guards.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-success.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-errors.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-success.test.ts create mode 100644 packages/api/src/__tests__/assistant-tools-holiday-mutation-errors.test.ts diff --git a/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-guards.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-guards.test.ts new file mode 100644 index 0000000..a752119 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-guards.test.ts @@ -0,0 +1,65 @@ +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([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +describe("assistant holiday calendar mutation tools - guards", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("rejects holiday calendar mutations for non-admin assistant users", async () => { + const ctx = createToolContext({}, [], SystemRole.MANAGER); + const result = await executeTool( + "create_holiday_calendar", + JSON.stringify({ + name: "Hamburg Feiertage", + scopeType: "STATE", + countryId: "country_de", + stateCode: "HH", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual( + expect.objectContaining({ + error: "You do not have permission to perform this action.", + }), + ); + }); + + it("returns a stable error when a holiday calendar scope already exists", async () => { + const ctx = createToolContext({ + country: { + findUnique: vi.fn().mockResolvedValue({ id: "country_de", name: "Germany" }), + }, + holidayCalendar: { + findFirst: vi.fn().mockResolvedValue({ id: "cal_existing" }), + }, + }); + + const result = await executeTool( + "create_holiday_calendar", + JSON.stringify({ name: "Germany National", scopeType: "COUNTRY", countryId: "country_de" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "A holiday calendar for this scope already exists.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-success.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-success.test.ts new file mode 100644 index 0000000..4044f14 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-calendar-mutations-success.test.ts @@ -0,0 +1,176 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +describe("assistant holiday calendar mutation tools - success", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates a holiday calendar through the assistant for admin users", async () => { + const ctx = createToolContext({ + country: { + findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }), + }, + holidayCalendar: { + findFirst: vi.fn().mockResolvedValue(null), + create: vi.fn().mockResolvedValue({ + id: "cal_by", + name: "Bayern Feiertage", + scopeType: "STATE", + stateCode: "BY", + isActive: true, + priority: 10, + country: { id: "country_de", code: "DE", name: "Deutschland" }, + metroCity: null, + entries: [], + }), + }, + }); + + const result = await executeTool( + "create_holiday_calendar", + JSON.stringify({ + name: "Bayern Feiertage", + scopeType: "STATE", + countryId: "country_de", + stateCode: "BY", + priority: 10, + }), + ctx, + ); + + const parsed = JSON.parse(result.content) as { + success: boolean; + message: string; + calendar: { name: string; stateCode: string | null }; + }; + + expect(parsed.success).toBe(true); + expect(parsed.message).toContain("Created holiday calendar"); + expect(parsed.calendar).toEqual( + expect.objectContaining({ + name: "Bayern Feiertage", + stateCode: "BY", + }), + ); + }); + + it("updates and deletes holiday calendars for admin users", async () => { + const holidayCalendarCreate = vi.fn().mockResolvedValue({ + id: "cal_de", + name: "Germany National", + scopeType: "COUNTRY", + stateCode: null, + isActive: true, + priority: 0, + country: { id: "country_de", code: "DE", name: "Germany" }, + metroCity: null, + entries: [], + }); + const holidayCalendarUpdate = vi.fn().mockResolvedValue({ + id: "cal_de", + name: "Germany National Updated", + scopeType: "COUNTRY", + stateCode: null, + isActive: false, + priority: 1, + country: { id: "country_de", code: "DE", name: "Germany" }, + metroCity: null, + entries: [], + }); + const holidayCalendarDelete = vi.fn().mockResolvedValue({ id: "cal_de" }); + const ctx = createToolContext({ + country: { + findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Germany" }), + }, + holidayCalendar: { + findFirst: vi.fn().mockResolvedValue(null), + findUnique: vi.fn() + .mockResolvedValueOnce({ + id: "cal_de", + name: "Germany National", + scopeType: "COUNTRY", + countryId: "country_de", + }) + .mockResolvedValueOnce({ + id: "cal_de", + name: "Germany National Updated", + scopeType: "COUNTRY", + countryId: "country_de", + }), + create: holidayCalendarCreate, + update: holidayCalendarUpdate, + delete: holidayCalendarDelete, + }, + }); + + const createCalendarResult = await executeTool( + "create_holiday_calendar", + JSON.stringify({ name: "Germany National", scopeType: "COUNTRY", countryId: "country_de" }), + ctx, + ); + const updateCalendarResult = await executeTool( + "update_holiday_calendar", + JSON.stringify({ id: "cal_de", data: { name: "Germany National Updated", isActive: false, priority: 1 } }), + ctx, + ); + const deleteCalendarResult = await executeTool( + "delete_holiday_calendar", + JSON.stringify({ id: "cal_de" }), + ctx, + ); + + expect(holidayCalendarCreate).toHaveBeenCalledWith({ + data: { + name: "Germany National", + scopeType: "COUNTRY", + countryId: "country_de", + isActive: true, + priority: 0, + }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(holidayCalendarUpdate).toHaveBeenCalledWith({ + where: { id: "cal_de" }, + data: { + name: "Germany National Updated", + isActive: false, + priority: 1, + }, + include: { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, + }, + }); + expect(holidayCalendarDelete).toHaveBeenCalledWith({ where: { id: "cal_de" } }); + expect(JSON.parse(createCalendarResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Created holiday calendar: Germany National" }), + ); + expect(JSON.parse(updateCalendarResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Updated holiday calendar: Germany National Updated" }), + ); + expect(JSON.parse(deleteCalendarResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Deleted holiday calendar: Germany National Updated" }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-errors.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-errors.test.ts new file mode 100644 index 0000000..5310939 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-errors.test.ts @@ -0,0 +1,90 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +describe("assistant holiday entry mutation tools - errors", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a stable error when a holiday entry calendar cannot be found", async () => { + const ctx = createToolContext({ + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }); + + const result = await executeTool( + "create_holiday_calendar_entry", + JSON.stringify({ + holidayCalendarId: "cal_missing", + date: "2026-01-01", + name: "New Year", + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Holiday calendar not found with the given criteria.", + }); + }); + + it("returns a stable error when a holiday entry date conflicts during update", async () => { + const ctx = createToolContext({ + holidayCalendarEntry: { + findUnique: vi.fn().mockResolvedValue({ + id: "entry_1", + name: "New Year", + date: new Date("2026-01-01T00:00:00.000Z"), + holidayCalendarId: "cal_de", + }), + findFirst: vi.fn().mockResolvedValue({ id: "entry_conflict" }), + }, + }); + + const result = await executeTool( + "update_holiday_calendar_entry", + JSON.stringify({ + id: "entry_1", + data: { date: "2026-01-02" }, + }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "A holiday entry for this calendar and date already exists.", + }); + }); + + it("returns a stable error when deleting a missing holiday calendar entry", async () => { + const ctx = createToolContext({ + holidayCalendarEntry: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }); + + const result = await executeTool( + "delete_holiday_calendar_entry", + JSON.stringify({ id: "entry_missing" }), + ctx, + ); + + expect(JSON.parse(result.content)).toEqual({ + error: "Holiday calendar entry not found with the given criteria.", + }); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-success.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-success.test.ts new file mode 100644 index 0000000..bb00834 --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-entry-mutations-success.test.ts @@ -0,0 +1,124 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +describe("assistant holiday entry mutation tools - success", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("creates, updates, and deletes holiday calendar entries for admin users", async () => { + const holidayEntryCreate = vi.fn().mockResolvedValue({ + id: "entry_1", + date: new Date("2026-01-01T00:00:00.000Z"), + name: "New Year", + isRecurringAnnual: true, + source: "seed", + }); + const holidayEntryUpdate = vi.fn().mockResolvedValue({ + id: "entry_1", + date: new Date("2026-01-02T00:00:00.000Z"), + name: "New Year Observed", + isRecurringAnnual: false, + source: null, + }); + const holidayEntryDelete = vi.fn().mockResolvedValue({ id: "entry_1" }); + const ctx = createToolContext({ + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue({ + id: "cal_de", + name: "Germany National Updated", + scopeType: "COUNTRY", + countryId: "country_de", + }), + }, + holidayCalendarEntry: { + findFirst: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(null), + findUnique: vi.fn() + .mockResolvedValueOnce({ + id: "entry_1", + name: "New Year", + date: new Date("2026-01-01T00:00:00.000Z"), + holidayCalendarId: "cal_de", + }) + .mockResolvedValueOnce({ + id: "entry_1", + name: "New Year Observed", + }), + create: holidayEntryCreate, + update: holidayEntryUpdate, + delete: holidayEntryDelete, + }, + }); + + const createEntryResult = await executeTool( + "create_holiday_calendar_entry", + JSON.stringify({ + holidayCalendarId: "cal_de", + date: "2026-01-01", + name: "New Year", + isRecurringAnnual: true, + source: "seed", + }), + ctx, + ); + const updateEntryResult = await executeTool( + "update_holiday_calendar_entry", + JSON.stringify({ + id: "entry_1", + data: { date: "2026-01-02", name: "New Year Observed", isRecurringAnnual: false, source: null }, + }), + ctx, + ); + const deleteEntryResult = await executeTool( + "delete_holiday_calendar_entry", + JSON.stringify({ id: "entry_1" }), + ctx, + ); + + expect(holidayEntryCreate).toHaveBeenCalledWith({ + data: { + holidayCalendarId: "cal_de", + date: new Date("2026-01-01T00:00:00.000Z"), + name: "New Year", + isRecurringAnnual: true, + source: "seed", + }, + }); + expect(holidayEntryUpdate).toHaveBeenCalledWith({ + where: { id: "entry_1" }, + data: { + date: new Date("2026-01-02T00:00:00.000Z"), + name: "New Year Observed", + isRecurringAnnual: false, + source: null, + }, + }); + expect(holidayEntryDelete).toHaveBeenCalledWith({ where: { id: "entry_1" } }); + expect(JSON.parse(createEntryResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Created holiday entry: New Year" }), + ); + expect(JSON.parse(updateEntryResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Updated holiday entry: New Year Observed" }), + ); + expect(JSON.parse(deleteEntryResult.content)).toEqual( + expect.objectContaining({ success: true, message: "Deleted holiday entry: New Year Observed" }), + ); + }); +}); diff --git a/packages/api/src/__tests__/assistant-tools-holiday-mutation-errors.test.ts b/packages/api/src/__tests__/assistant-tools-holiday-mutation-errors.test.ts new file mode 100644 index 0000000..a04f83f --- /dev/null +++ b/packages/api/src/__tests__/assistant-tools-holiday-mutation-errors.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@capakraken/application", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDashboardBudgetForecast: vi.fn().mockResolvedValue([]), + listAssignmentBookings: vi.fn().mockResolvedValue([]), + }; +}); + +vi.mock("../lib/audit.js", () => ({ + createAuditEntry: vi.fn().mockResolvedValue(undefined), +})); + +import { executeTool } from "../router/assistant-tools.js"; +import { createToolContext } from "./assistant-tools-holiday-test-helpers.js"; + +describe("assistant holiday mutation error tools", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns stable assistant errors for holiday calendar and entry mutations", async () => { + const cases = [ + { + name: "invalid holiday calendar scope", + toolName: "create_holiday_calendar", + db: { + country: { + findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }), + }, + holidayCalendar: { + findFirst: vi.fn().mockResolvedValue(null), + }, + }, + payload: { + name: "Ungueltiger Kalender", + scopeType: "STATE", + countryId: "country_de", + }, + expected: "Holiday calendar scope is invalid.", + }, + { + name: "duplicate holiday calendar scope", + toolName: "create_holiday_calendar", + db: { + country: { + findUnique: vi.fn().mockResolvedValue({ id: "country_de", code: "DE", name: "Deutschland" }), + }, + holidayCalendar: { + findFirst: vi.fn().mockResolvedValue({ id: "cal_existing" }), + }, + }, + payload: { + name: "Bayern Feiertage", + scopeType: "STATE", + countryId: "country_de", + stateCode: "BY", + }, + expected: "A holiday calendar for this scope already exists.", + }, + { + name: "holiday calendar not found on delete", + toolName: "delete_holiday_calendar", + db: { + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + payload: { id: "cal_missing" }, + expected: "Holiday calendar not found with the given criteria.", + }, + { + name: "holiday calendar entry not found on delete", + toolName: "delete_holiday_calendar_entry", + db: { + holidayCalendarEntry: { + findUnique: vi.fn().mockResolvedValue(null), + }, + }, + payload: { id: "entry_missing" }, + expected: "Holiday calendar entry not found with the given criteria.", + }, + { + name: "duplicate holiday calendar entry date", + toolName: "create_holiday_calendar_entry", + db: { + holidayCalendar: { + findUnique: vi.fn().mockResolvedValue({ id: "cal_by", name: "Bayern Feiertage" }), + }, + holidayCalendarEntry: { + findFirst: vi.fn().mockResolvedValue({ id: "entry_existing" }), + }, + }, + payload: { + holidayCalendarId: "cal_by", + date: "2026-01-06", + name: "Heilige Drei Koenige", + }, + expected: "A holiday entry for this calendar and date already exists.", + }, + ] as const; + + for (const testCase of cases) { + const result = await executeTool( + testCase.toolName, + JSON.stringify(testCase.payload), + createToolContext(testCase.db), + ); + + expect(JSON.parse(result.content)).toEqual({ + error: testCase.expected, + }); + } + }); +});