a0de69a520
Covers comment CRUD/resolve/delete, project status transitions and cascade deletes, dispo import batch read/cancel/commit/resolve, and holiday calendar catalog read with identifier fallback lookup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
import { SystemRole } from "@capakraken/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createCallerFactory, createTRPCRouter } from "../trpc.js";
|
|
import { holidayCalendarCatalogReadProcedures } from "../router/holiday-calendar-catalog-read.js";
|
|
|
|
// Pass-through so the db mock reaches the procedures unchanged
|
|
vi.mock("../router/holiday-calendar-shared.js", () => ({
|
|
asHolidayCalendarDb: (db: unknown) => db,
|
|
}));
|
|
|
|
// Provide the same shape the real support module exports
|
|
vi.mock("../router/holiday-calendar-support.js", () => ({
|
|
holidayCalendarListInclude: {
|
|
country: { select: { id: true, code: true, name: true } },
|
|
metroCity: { select: { id: true, name: true } },
|
|
entries: { orderBy: [{ date: "asc" }, { name: "asc" }] },
|
|
_count: { select: { entries: true } },
|
|
},
|
|
holidayCalendarDetailInclude: {
|
|
country: { select: { id: true, code: true, name: true } },
|
|
metroCity: { select: { id: true, name: true } },
|
|
entries: { orderBy: [{ date: "asc" }, { name: "asc" }] },
|
|
},
|
|
}));
|
|
|
|
const router = createTRPCRouter(holidayCalendarCatalogReadProcedures);
|
|
const createCaller = createCallerFactory(router);
|
|
|
|
function createAdminCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "admin@example.com", name: "Admin", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null },
|
|
});
|
|
}
|
|
|
|
function createUserCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: { id: "user_1", systemRole: SystemRole.USER, permissionOverrides: null },
|
|
});
|
|
}
|
|
|
|
const mockCalendar = {
|
|
id: "cal_1",
|
|
name: "German Federal Holidays",
|
|
scopeType: "COUNTRY",
|
|
stateCode: null,
|
|
isActive: true,
|
|
priority: 0,
|
|
country: { id: "country_de", code: "DE", name: "Germany" },
|
|
metroCity: null,
|
|
_count: { entries: 12 },
|
|
entries: [
|
|
{
|
|
id: "entry_1",
|
|
date: new Date("2026-01-01"),
|
|
name: "New Year",
|
|
isRecurringAnnual: true,
|
|
source: null,
|
|
},
|
|
],
|
|
};
|
|
|
|
// ─── listCalendars ────────────────────────────────────────────────────────────
|
|
|
|
describe("listCalendars", () => {
|
|
it("returns all active calendars when called with no input", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([mockCalendar]);
|
|
const caller = createAdminCaller({ holidayCalendar: { findMany } });
|
|
|
|
const result = await caller.listCalendars();
|
|
|
|
expect(result).toEqual([mockCalendar]);
|
|
expect(findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({ isActive: true }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("applies countryCode filter uppercased with case-insensitive mode", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([mockCalendar]);
|
|
const caller = createAdminCaller({ holidayCalendar: { findMany } });
|
|
|
|
await caller.listCalendars({ countryCode: "de" });
|
|
|
|
expect(findMany).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: expect.objectContaining({
|
|
country: { code: { equals: "DE", mode: "insensitive" } },
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes inactive calendars when includeInactive is true", async () => {
|
|
const inactiveCalendar = { ...mockCalendar, id: "cal_inactive", isActive: false };
|
|
const findMany = vi.fn().mockResolvedValue([mockCalendar, inactiveCalendar]);
|
|
const caller = createAdminCaller({ holidayCalendar: { findMany } });
|
|
|
|
const result = await caller.listCalendars({ includeInactive: true });
|
|
|
|
expect(result).toHaveLength(2);
|
|
// isActive filter must NOT be present in the where clause
|
|
const whereArg = findMany.mock.calls[0][0].where;
|
|
expect(whereArg).not.toHaveProperty("isActive");
|
|
});
|
|
});
|
|
|
|
// ─── listCalendarsDetail ──────────────────────────────────────────────────────
|
|
|
|
describe("listCalendarsDetail", () => {
|
|
it("returns count and formatted calendar array", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([mockCalendar]);
|
|
const caller = createAdminCaller({ holidayCalendar: { findMany } });
|
|
|
|
const result = await caller.listCalendarsDetail();
|
|
|
|
expect(result.count).toBe(1);
|
|
expect(result.calendars).toHaveLength(1);
|
|
expect(result.calendars[0]).toMatchObject({
|
|
id: "cal_1",
|
|
name: "German Federal Holidays",
|
|
scopeType: "COUNTRY",
|
|
stateCode: null,
|
|
isActive: true,
|
|
priority: 0,
|
|
country: { id: "country_de", code: "DE", name: "Germany" },
|
|
metroCity: null,
|
|
entryCount: 12,
|
|
});
|
|
});
|
|
|
|
it("formats entry dates as ISO date strings (YYYY-MM-DD)", async () => {
|
|
const findMany = vi.fn().mockResolvedValue([mockCalendar]);
|
|
const caller = createAdminCaller({ holidayCalendar: { findMany } });
|
|
|
|
const result = await caller.listCalendarsDetail();
|
|
|
|
expect(result.calendars[0].entries[0]).toMatchObject({
|
|
id: "entry_1",
|
|
date: "2026-01-01",
|
|
name: "New Year",
|
|
isRecurringAnnual: true,
|
|
source: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── getCalendarByIdentifier ──────────────────────────────────────────────────
|
|
|
|
describe("getCalendarByIdentifier", () => {
|
|
it("finds a calendar by exact id on the first lookup", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(mockCalendar);
|
|
const findFirst = vi.fn();
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst } });
|
|
|
|
const result = await caller.getCalendarByIdentifier({ identifier: "cal_1" });
|
|
|
|
expect(result).toEqual(mockCalendar);
|
|
expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "cal_1" } }));
|
|
expect(findFirst).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("falls back to exact name match when id lookup returns null", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const findFirst = vi.fn().mockResolvedValueOnce(mockCalendar);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst } });
|
|
|
|
const result = await caller.getCalendarByIdentifier({ identifier: "German Federal Holidays" });
|
|
|
|
expect(result).toEqual(mockCalendar);
|
|
expect(findFirst).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { name: { equals: "German Federal Holidays", mode: "insensitive" } },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("falls back to contains name match when exact name lookup returns null", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const findFirst = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null) // exact name miss
|
|
.mockResolvedValueOnce(mockCalendar); // contains name hit
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst } });
|
|
|
|
const result = await caller.getCalendarByIdentifier({ identifier: "Federal" });
|
|
|
|
expect(result).toEqual(mockCalendar);
|
|
expect(findFirst).toHaveBeenCalledTimes(2);
|
|
expect(findFirst).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
where: { name: { contains: "Federal", mode: "insensitive" } },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("throws NOT_FOUND when no match is found at all", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const findFirst = vi.fn().mockResolvedValue(null);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst } });
|
|
|
|
await expect(
|
|
caller.getCalendarByIdentifier({ identifier: "does-not-exist" }),
|
|
).rejects.toMatchObject({
|
|
code: "NOT_FOUND",
|
|
message: "Holiday calendar not found: does-not-exist",
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── getCalendarByIdentifierDetail ────────────────────────────────────────────
|
|
|
|
describe("getCalendarByIdentifierDetail", () => {
|
|
it("returns formatted detail for a matching calendar", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(mockCalendar);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst: vi.fn() } });
|
|
|
|
const result = await caller.getCalendarByIdentifierDetail({ identifier: "cal_1" });
|
|
|
|
expect(result).toMatchObject({
|
|
id: "cal_1",
|
|
name: "German Federal Holidays",
|
|
scopeType: "COUNTRY",
|
|
isActive: true,
|
|
entryCount: 12,
|
|
entries: [expect.objectContaining({ date: "2026-01-01" })],
|
|
});
|
|
});
|
|
|
|
it("throws NOT_FOUND when identifier does not match any calendar", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const findFirst = vi.fn().mockResolvedValue(null);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique, findFirst } });
|
|
|
|
await expect(
|
|
caller.getCalendarByIdentifierDetail({ identifier: "no-such-calendar" }),
|
|
).rejects.toMatchObject({ code: "NOT_FOUND" });
|
|
});
|
|
});
|
|
|
|
// ─── getCalendarById ──────────────────────────────────────────────────────────
|
|
|
|
describe("getCalendarById", () => {
|
|
it("returns the calendar when found by id", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(mockCalendar);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique } });
|
|
|
|
const result = await caller.getCalendarById({ id: "cal_1" });
|
|
|
|
expect(result).toEqual(mockCalendar);
|
|
expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "cal_1" } }));
|
|
});
|
|
|
|
it("throws NOT_FOUND when no calendar exists with the given id", async () => {
|
|
const findUnique = vi.fn().mockResolvedValue(null);
|
|
const caller = createAdminCaller({ holidayCalendar: { findUnique } });
|
|
|
|
await expect(caller.getCalendarById({ id: "cal_missing" })).rejects.toMatchObject({
|
|
code: "NOT_FOUND",
|
|
message: "Holiday calendar not found",
|
|
});
|
|
});
|
|
});
|
|
|
|
// ─── Authorization ────────────────────────────────────────────────────────────
|
|
|
|
describe("Authorization", () => {
|
|
it("rejects a non-admin user (SystemRole.USER) with FORBIDDEN on all procedures", async () => {
|
|
const findMany = vi.fn();
|
|
const findUnique = vi.fn();
|
|
const findFirst = vi.fn();
|
|
const caller = createUserCaller({ holidayCalendar: { findMany, findUnique, findFirst } });
|
|
|
|
await expect(caller.listCalendars()).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Admin role required",
|
|
});
|
|
await expect(caller.listCalendarsDetail()).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Admin role required",
|
|
});
|
|
await expect(caller.getCalendarByIdentifier({ identifier: "cal_1" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Admin role required",
|
|
});
|
|
await expect(
|
|
caller.getCalendarByIdentifierDetail({ identifier: "cal_1" }),
|
|
).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Admin role required",
|
|
});
|
|
await expect(caller.getCalendarById({ id: "cal_1" })).rejects.toMatchObject({
|
|
code: "FORBIDDEN",
|
|
message: "Admin role required",
|
|
});
|
|
|
|
expect(findMany).not.toHaveBeenCalled();
|
|
expect(findUnique).not.toHaveBeenCalled();
|
|
expect(findFirst).not.toHaveBeenCalled();
|
|
});
|
|
});
|