test(api): add 68 router tests for comment, project-lifecycle, dispo, holiday-calendar
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>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user