127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import { VacationStatus } from "@capakraken/db";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
approvableVacationStatuses,
|
|
assertVacationApprovable,
|
|
assertVacationCancelable,
|
|
assertVacationRejectable,
|
|
buildApprovedVacationUpdateData,
|
|
buildRejectedVacationUpdateData,
|
|
buildVacationStatusUpdateData,
|
|
canActorCancelVacation,
|
|
isVacationManagerRole,
|
|
} from "../router/vacation-management-support.js";
|
|
|
|
describe("vacation management support", () => {
|
|
it("exposes approvable statuses and guards invalid transitions", () => {
|
|
expect(approvableVacationStatuses).toEqual([
|
|
VacationStatus.PENDING,
|
|
VacationStatus.CANCELLED,
|
|
VacationStatus.REJECTED,
|
|
]);
|
|
|
|
expect(() => assertVacationApprovable(VacationStatus.PENDING)).not.toThrow();
|
|
expect(() => assertVacationRejectable(VacationStatus.PENDING)).not.toThrow();
|
|
expect(() => assertVacationCancelable(VacationStatus.PENDING)).not.toThrow();
|
|
|
|
expect(() => assertVacationApprovable(VacationStatus.APPROVED)).toThrowError(
|
|
new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Only PENDING, CANCELLED, or REJECTED vacations can be approved",
|
|
}),
|
|
);
|
|
expect(() => assertVacationRejectable(VacationStatus.APPROVED)).toThrowError(
|
|
new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Only PENDING vacations can be rejected",
|
|
}),
|
|
);
|
|
expect(() => assertVacationCancelable(VacationStatus.CANCELLED)).toThrowError(
|
|
new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Already cancelled",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("builds approval, rejection, and generic status payloads", () => {
|
|
const approvedAt = new Date("2026-02-01T10:00:00.000Z");
|
|
|
|
expect(buildApprovedVacationUpdateData({
|
|
deductionSnapshotWriteData: { deductedDays: 3, deductionSnapshot: { reason: "calendar" } },
|
|
approvedById: "user_mgr",
|
|
approvedAt,
|
|
})).toEqual({
|
|
status: VacationStatus.APPROVED,
|
|
rejectionReason: null,
|
|
deductedDays: 3,
|
|
deductionSnapshot: { reason: "calendar" },
|
|
approvedById: "user_mgr",
|
|
approvedAt,
|
|
});
|
|
|
|
expect(buildRejectedVacationUpdateData({
|
|
rejectionReason: "Capacity conflict",
|
|
})).toEqual({
|
|
status: VacationStatus.REJECTED,
|
|
rejectionReason: "Capacity conflict",
|
|
});
|
|
|
|
expect(buildVacationStatusUpdateData({
|
|
status: "APPROVED",
|
|
note: "Reviewed",
|
|
approvedById: "user_mgr",
|
|
approvedAt,
|
|
})).toEqual({
|
|
status: "APPROVED",
|
|
approvedById: "user_mgr",
|
|
approvedAt,
|
|
rejectionReason: null,
|
|
note: "Reviewed",
|
|
});
|
|
|
|
expect(buildVacationStatusUpdateData({
|
|
status: "CANCELLED",
|
|
approvedById: "user_mgr",
|
|
approvedAt,
|
|
})).toEqual({
|
|
status: "CANCELLED",
|
|
});
|
|
});
|
|
|
|
it("captures manager bypass and ownership-based cancellation access", () => {
|
|
expect(isVacationManagerRole("ADMIN")).toBe(true);
|
|
expect(isVacationManagerRole("MANAGER")).toBe(true);
|
|
expect(isVacationManagerRole("USER")).toBe(false);
|
|
|
|
expect(canActorCancelVacation({
|
|
actorId: "user_1",
|
|
actorRole: "USER",
|
|
requestedById: "user_1",
|
|
resourceUserId: "user_2",
|
|
})).toBe(true);
|
|
|
|
expect(canActorCancelVacation({
|
|
actorId: "user_1",
|
|
actorRole: "USER",
|
|
requestedById: "user_2",
|
|
resourceUserId: "user_1",
|
|
})).toBe(true);
|
|
|
|
expect(canActorCancelVacation({
|
|
actorId: "user_mgr",
|
|
actorRole: "MANAGER",
|
|
requestedById: "user_2",
|
|
resourceUserId: null,
|
|
})).toBe(true);
|
|
|
|
expect(canActorCancelVacation({
|
|
actorId: "user_1",
|
|
actorRole: "USER",
|
|
requestedById: "user_2",
|
|
resourceUserId: "user_3",
|
|
})).toBe(false);
|
|
});
|
|
});
|