test(api): add 34 router tests for estimate read/workflow and vacation read
Covers estimate list, getById, version snapshot aggregation, rethrowEstimateRouterError, submit/approve/createRevision workflow procedures. Vacation read covers isSameUtcDay, list, getById, getForResource, team overlap, and team overlap detail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,417 @@
|
|||||||
|
import { SystemRole } from "@capakraken/shared";
|
||||||
|
import { VacationStatus, VacationType } from "@capakraken/db";
|
||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
// Mock the vacation read support
|
||||||
|
const buildVacationPreviewMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ workingDays: 5, holidays: 1, totalDays: 7 });
|
||||||
|
const findVacationResourceChapterMock = vi.fn();
|
||||||
|
const listChapterVacationOverlapsMock = vi.fn().mockResolvedValue([]);
|
||||||
|
|
||||||
|
vi.mock("../router/vacation-read-support.js", () => ({
|
||||||
|
buildVacationPreview: (...args: unknown[]) => buildVacationPreviewMock(...args),
|
||||||
|
findVacationResourceChapter: (...args: unknown[]) => findVacationResourceChapterMock(...args),
|
||||||
|
listChapterVacationOverlaps: (...args: unknown[]) => listChapterVacationOverlapsMock(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock resource owned read access
|
||||||
|
vi.mock("../router/resource-owned-read-access.js", () => ({
|
||||||
|
assertCanReadOwnedResource: vi.fn().mockResolvedValue(undefined),
|
||||||
|
canManageOwnedResourceReads: vi.fn().mockReturnValue(true),
|
||||||
|
resolveOwnedResourceReadFilter: vi.fn().mockResolvedValue("resource_1"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock resource holiday context
|
||||||
|
vi.mock("../lib/resource-holiday-context.js", () => ({
|
||||||
|
loadResourceHolidayContext: vi.fn().mockResolvedValue({ holidays: [] }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock anonymization (pass-through)
|
||||||
|
vi.mock("../lib/anonymization.js", () => ({
|
||||||
|
anonymizeResource: (r: unknown) => r,
|
||||||
|
anonymizeUser: (u: unknown) => u,
|
||||||
|
getAnonymizationDirectory: vi.fn().mockResolvedValue(null),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock db helpers
|
||||||
|
vi.mock("../db/helpers.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../db/helpers.js")>();
|
||||||
|
return actual;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock selects
|
||||||
|
vi.mock("../db/selects.js", () => ({
|
||||||
|
RESOURCE_BRIEF_SELECT: { id: true, displayName: true, eid: true, lcrCents: true, chapter: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { isSameUtcDay } from "../router/vacation-read.js";
|
||||||
|
import { createCallerFactory, createTRPCRouter } from "../trpc.js";
|
||||||
|
import { vacationReadProcedures } from "../router/vacation-read.js";
|
||||||
|
|
||||||
|
const router = createTRPCRouter(vacationReadProcedures);
|
||||||
|
const createCaller = createCallerFactory(router);
|
||||||
|
|
||||||
|
function createManagerCaller(db: Record<string, unknown>) {
|
||||||
|
return createCaller({
|
||||||
|
session: {
|
||||||
|
user: { email: "mgr@example.com", name: "Manager", image: null },
|
||||||
|
expires: "2099-01-01T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
db: db as never,
|
||||||
|
dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockVacation = {
|
||||||
|
id: "vac_1",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
type: VacationType.VACATION,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
requestedById: "user_mgr",
|
||||||
|
resource: {
|
||||||
|
id: "resource_1",
|
||||||
|
displayName: "Test User",
|
||||||
|
eid: "E001",
|
||||||
|
lcrCents: 5000,
|
||||||
|
chapter: "Engineering",
|
||||||
|
userId: "user_mgr",
|
||||||
|
},
|
||||||
|
requestedBy: { id: "user_mgr", name: "Manager", email: "mgr@example.com" },
|
||||||
|
approvedBy: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("isSameUtcDay", () => {
|
||||||
|
it("returns true for the same UTC day", () => {
|
||||||
|
const a = new Date("2026-06-15T00:00:00.000Z");
|
||||||
|
const b = new Date("2026-06-15T00:00:00.000Z");
|
||||||
|
expect(isSameUtcDay(a, b)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for different UTC days", () => {
|
||||||
|
const a = new Date("2026-06-15T00:00:00.000Z");
|
||||||
|
const b = new Date("2026-06-16T00:00:00.000Z");
|
||||||
|
expect(isSameUtcDay(a, b)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for same UTC day with different times", () => {
|
||||||
|
const a = new Date("2026-06-15T08:00:00.000Z");
|
||||||
|
const b = new Date("2026-06-15T23:59:59.999Z");
|
||||||
|
expect(isSameUtcDay(a, b)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("list", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
// Re-apply stable defaults after clearAllMocks
|
||||||
|
const { resolveOwnedResourceReadFilter, canManageOwnedResourceReads } = vi.mocked(
|
||||||
|
await import("../router/resource-owned-read-access.js"),
|
||||||
|
);
|
||||||
|
resolveOwnedResourceReadFilter.mockResolvedValue("resource_1");
|
||||||
|
canManageOwnedResourceReads.mockReturnValue(true);
|
||||||
|
const { getAnonymizationDirectory } = vi.mocked(await import("../lib/anonymization.js"));
|
||||||
|
getAnonymizationDirectory.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns vacations filtered by resource with date range and status filter", async () => {
|
||||||
|
const findMany = vi.fn().mockResolvedValue([mockVacation]);
|
||||||
|
const caller = createManagerCaller({ vacation: { findMany } });
|
||||||
|
|
||||||
|
const result = await caller.list({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-30"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
endDate: { gte: new Date("2026-06-01") },
|
||||||
|
startDate: { lte: new Date("2026-06-30") },
|
||||||
|
}),
|
||||||
|
orderBy: { startDate: "asc" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]!.id).toBe("vac_1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies the default limit of 100 when limit is not specified", async () => {
|
||||||
|
const findMany = vi.fn().mockResolvedValue([]);
|
||||||
|
const caller = createManagerCaller({ vacation: { findMany } });
|
||||||
|
|
||||||
|
await caller.list({});
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 100 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects an explicit limit parameter", async () => {
|
||||||
|
const findMany = vi.fn().mockResolvedValue([mockVacation]);
|
||||||
|
const caller = createManagerCaller({ vacation: { findMany } });
|
||||||
|
|
||||||
|
await caller.list({ limit: 10 });
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalledWith(expect.objectContaining({ take: 10 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getById", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const { canManageOwnedResourceReads } = vi.mocked(
|
||||||
|
await import("../router/resource-owned-read-access.js"),
|
||||||
|
);
|
||||||
|
canManageOwnedResourceReads.mockReturnValue(true);
|
||||||
|
const { getAnonymizationDirectory } = vi.mocked(await import("../lib/anonymization.js"));
|
||||||
|
getAnonymizationDirectory.mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns vacation with anonymized data when found", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(mockVacation);
|
||||||
|
const caller = createManagerCaller({ vacation: { findUnique } });
|
||||||
|
|
||||||
|
const result = await caller.getById({ id: "vac_1" });
|
||||||
|
|
||||||
|
expect(findUnique).toHaveBeenCalledWith(expect.objectContaining({ where: { id: "vac_1" } }));
|
||||||
|
expect(result.id).toBe("vac_1");
|
||||||
|
expect(result.resource).toMatchObject({
|
||||||
|
id: "resource_1",
|
||||||
|
displayName: "Test User",
|
||||||
|
eid: "E001",
|
||||||
|
lcrCents: 5000,
|
||||||
|
chapter: "Engineering",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws NOT_FOUND when vacation does not exist", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
const caller = createManagerCaller({ vacation: { findUnique } });
|
||||||
|
|
||||||
|
await expect(caller.getById({ id: "vac_missing" })).rejects.toMatchObject({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Vacation not found",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips userId from the returned resource shape", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(mockVacation);
|
||||||
|
const caller = createManagerCaller({ vacation: { findUnique } });
|
||||||
|
|
||||||
|
const result = await caller.getById({ id: "vac_1" });
|
||||||
|
|
||||||
|
expect(result.resource).not.toHaveProperty("userId");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getForResource", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const { assertCanReadOwnedResource } = vi.mocked(
|
||||||
|
await import("../router/resource-owned-read-access.js"),
|
||||||
|
);
|
||||||
|
assertCanReadOwnedResource.mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns approved vacations in the requested date range", async () => {
|
||||||
|
const findMany = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "vac_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
type: VacationType.VACATION,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const caller = createManagerCaller({ vacation: { findMany } });
|
||||||
|
|
||||||
|
const result = await caller.getForResource({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-30"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: { lte: new Date("2026-06-30") },
|
||||||
|
endDate: { gte: new Date("2026-06-01") },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by APPROVED status only", async () => {
|
||||||
|
const findMany = vi.fn().mockResolvedValue([]);
|
||||||
|
const caller = createManagerCaller({ vacation: { findMany } });
|
||||||
|
|
||||||
|
await caller.getForResource({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-30"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getTeamOverlap", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const { assertCanReadOwnedResource } = vi.mocked(
|
||||||
|
await import("../router/resource-owned-read-access.js"),
|
||||||
|
);
|
||||||
|
assertCanReadOwnedResource.mockResolvedValue(undefined);
|
||||||
|
listChapterVacationOverlapsMock.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array when resource has no chapter", async () => {
|
||||||
|
findVacationResourceChapterMock.mockResolvedValue(null);
|
||||||
|
const caller = createManagerCaller({});
|
||||||
|
|
||||||
|
const result = await caller.getTeamOverlap({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(listChapterVacationOverlapsMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls listChapterVacationOverlaps when chapter exists", async () => {
|
||||||
|
findVacationResourceChapterMock.mockResolvedValue("Engineering");
|
||||||
|
const overlapData = [
|
||||||
|
{
|
||||||
|
id: "vac_2",
|
||||||
|
resourceId: "resource_2",
|
||||||
|
startDate: new Date("2026-06-02"),
|
||||||
|
endDate: new Date("2026-06-03"),
|
||||||
|
type: VacationType.VACATION,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
listChapterVacationOverlapsMock.mockResolvedValue(overlapData);
|
||||||
|
const caller = createManagerCaller({});
|
||||||
|
|
||||||
|
const result = await caller.getTeamOverlap({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listChapterVacationOverlapsMock).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
chapter: "Engineering",
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(result).toEqual(overlapData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getTeamOverlapDetail", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const { assertCanReadOwnedResource } = vi.mocked(
|
||||||
|
await import("../router/resource-owned-read-access.js"),
|
||||||
|
);
|
||||||
|
assertCanReadOwnedResource.mockResolvedValue(undefined);
|
||||||
|
listChapterVacationOverlapsMock.mockResolvedValue([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws NOT_FOUND when resource does not exist", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue(null);
|
||||||
|
const caller = createManagerCaller({ resource: { findUnique } });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
caller.getTeamOverlapDetail({
|
||||||
|
resourceId: "resource_missing",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Resource not found",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns formatted detail with empty overlaps when resource has no chapter", async () => {
|
||||||
|
const findUnique = vi.fn().mockResolvedValue({ displayName: "Test User", chapter: null });
|
||||||
|
const caller = createManagerCaller({ resource: { findUnique } });
|
||||||
|
|
||||||
|
const result = await caller.getTeamOverlapDetail({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
resource: "Test User",
|
||||||
|
chapter: null,
|
||||||
|
period: "2026-06-01 to 2026-06-05",
|
||||||
|
overlappingVacations: [],
|
||||||
|
overlapCount: 0,
|
||||||
|
});
|
||||||
|
expect(listChapterVacationOverlapsMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns formatted detail with overlaps when chapter exists", async () => {
|
||||||
|
const findUnique = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ displayName: "Test User", chapter: "Engineering" });
|
||||||
|
const overlap = {
|
||||||
|
type: VacationType.VACATION,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
startDate: new Date("2026-06-02"),
|
||||||
|
endDate: new Date("2026-06-03"),
|
||||||
|
resource: { displayName: "Colleague A" },
|
||||||
|
};
|
||||||
|
listChapterVacationOverlapsMock.mockResolvedValue([overlap]);
|
||||||
|
const caller = createManagerCaller({ resource: { findUnique } });
|
||||||
|
|
||||||
|
const result = await caller.getTeamOverlapDetail({
|
||||||
|
resourceId: "resource_1",
|
||||||
|
startDate: new Date("2026-06-01"),
|
||||||
|
endDate: new Date("2026-06-05"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
resource: "Test User",
|
||||||
|
chapter: "Engineering",
|
||||||
|
period: "2026-06-01 to 2026-06-05",
|
||||||
|
overlapCount: 1,
|
||||||
|
overlappingVacations: [
|
||||||
|
{
|
||||||
|
resource: "Colleague A",
|
||||||
|
type: VacationType.VACATION,
|
||||||
|
status: VacationStatus.APPROVED,
|
||||||
|
start: "2026-06-02",
|
||||||
|
end: "2026-06-03",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(listChapterVacationOverlapsMock).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ chapter: "Engineering", resourceId: "resource_1" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user