feat(holiday-calendar): restrict catalog reads to admins

This commit is contained in:
2026-03-30 10:36:05 +02:00
parent 54769ca0f5
commit c2ca6a6d0d
5 changed files with 151 additions and 10 deletions
@@ -629,17 +629,21 @@ describe("assistant router tool gating", () => {
expect(userNames).toContain("get_ai_configured");
});
it("keeps holiday calendar mutation tools admin-only while leaving read tools available", () => {
it("keeps holiday calendar catalog tools admin-only while leaving preview available", () => {
const adminNames = getToolNames([], SystemRole.ADMIN);
const managerNames = getToolNames([], SystemRole.MANAGER);
const userNames = getToolNames([], SystemRole.USER);
expect(adminNames).toContain("list_holiday_calendars");
expect(adminNames).toContain("get_holiday_calendar");
expect(adminNames).toContain("preview_resolved_holiday_calendar");
expect(adminNames).toContain("create_holiday_calendar");
expect(managerNames).toContain("list_holiday_calendars");
expect(managerNames).toContain("get_holiday_calendar");
expect(managerNames).not.toContain("list_holiday_calendars");
expect(managerNames).not.toContain("get_holiday_calendar");
expect(managerNames).toContain("preview_resolved_holiday_calendar");
expect(userNames).not.toContain("list_holiday_calendars");
expect(userNames).not.toContain("get_holiday_calendar");
expect(userNames).toContain("preview_resolved_holiday_calendar");
expect(managerNames).not.toContain("create_holiday_calendar");
expect(managerNames).not.toContain("update_holiday_calendar");
expect(managerNames).not.toContain("delete_holiday_calendar");
@@ -40,6 +40,130 @@ function createAdminCaller(db: Record<string, unknown>) {
}
describe("holiday calendar router", () => {
it("requires admin access for holiday calendar catalog reads", async () => {
const findMany = vi.fn();
const findUnique = vi.fn();
const findFirst = vi.fn();
const caller = createProtectedCaller({
holidayCalendar: {
findMany,
findUnique,
findFirst,
},
});
await expect(caller.listCalendars({ includeInactive: true })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
await expect(caller.listCalendarsDetail({ includeInactive: true })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
await expect(caller.getCalendarByIdentifier({ identifier: "Deutschland" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
await expect(caller.getCalendarByIdentifierDetail({ identifier: "Deutschland" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
await expect(caller.getCalendarById({ id: "cal_de" })).rejects.toMatchObject({
code: "FORBIDDEN",
message: "Admin role required",
});
expect(findMany).not.toHaveBeenCalled();
expect(findUnique).not.toHaveBeenCalled();
expect(findFirst).not.toHaveBeenCalled();
});
it("allows admins to read holiday calendar catalog routes", async () => {
const listRows = [
{
id: "cal_de",
name: "Deutschland",
scopeType: "COUNTRY",
stateCode: null,
isActive: true,
priority: 0,
country: { id: "country_de", code: "DE", name: "Deutschland" },
metroCity: null,
_count: { entries: 1 },
entries: [
{
id: "entry_1",
date: new Date("2026-01-01T00:00:00.000Z"),
name: "Neujahr",
isRecurringAnnual: true,
source: "builtin",
},
],
},
];
const detailRow = {
id: "cal_de",
name: "Deutschland",
scopeType: "COUNTRY",
stateCode: null,
isActive: true,
priority: 0,
country: { id: "country_de", code: "DE", name: "Deutschland" },
metroCity: null,
entries: [
{
id: "entry_1",
date: new Date("2026-01-01T00:00:00.000Z"),
name: "Neujahr",
isRecurringAnnual: true,
source: "builtin",
},
],
};
const findMany = vi.fn().mockResolvedValue(listRows);
const findUnique = vi
.fn()
.mockResolvedValueOnce(detailRow)
.mockResolvedValueOnce(detailRow)
.mockResolvedValueOnce(detailRow);
const findFirst = vi.fn();
const caller = createAdminCaller({
holidayCalendar: {
findMany,
findUnique,
findFirst,
},
});
const listResult = await caller.listCalendars({ includeInactive: true });
const detailResult = await caller.listCalendarsDetail({ includeInactive: true });
const byIdentifierResult = await caller.getCalendarByIdentifier({ identifier: "cal_de" });
const byIdentifierDetailResult = await caller.getCalendarByIdentifierDetail({ identifier: "cal_de" });
const byIdResult = await caller.getCalendarById({ id: "cal_de" });
expect(listResult).toEqual(listRows);
expect(detailResult).toEqual({
count: 1,
calendars: [
expect.objectContaining({
id: "cal_de",
entryCount: 1,
}),
],
});
expect(byIdentifierResult).toEqual(detailRow);
expect(byIdentifierDetailResult).toEqual(
expect.objectContaining({
id: "cal_de",
entryCount: 1,
}),
);
expect(byIdResult).toEqual(detailRow);
expect(findMany).toHaveBeenCalledTimes(2);
expect(findUnique).toHaveBeenCalledTimes(3);
expect(findFirst).not.toHaveBeenCalled();
});
it("lists holiday calendars with assistant-facing detail formatting", async () => {
const db = {
holidayCalendar: {
@@ -68,7 +192,7 @@ describe("holiday calendar router", () => {
},
};
const caller = createProtectedCaller(db);
const caller = createAdminCaller(db);
const result = await caller.listCalendarsDetail({
countryCode: "DE",
scopeType: "STATE",
@@ -127,7 +251,7 @@ describe("holiday calendar router", () => {
},
};
const caller = createProtectedCaller(db);
const caller = createAdminCaller(db);
const result = await caller.getCalendarByIdentifierDetail({ identifier: "Augsburg lokal" });
expect(result).toEqual(