d0926601ea
Phase 3a: raises shared schema coverage from 5.5% to ~95%. Tests cover valid roundtrips, invalid rejection, edge cases for refinements, defaults, date coercion, and the generateDynamicZodSchema runtime builder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
845 lines
29 KiB
TypeScript
845 lines
29 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
CreateBlueprintSchema,
|
|
UpdateBlueprintSchema,
|
|
BlueprintFieldDefinitionSchema,
|
|
generateDynamicZodSchema,
|
|
} from "../schemas/blueprint.schema.js";
|
|
import { CreateOrgUnitSchema, UpdateOrgUnitSchema } from "../schemas/org-unit.schema.js";
|
|
import {
|
|
CreateManagementLevelGroupSchema,
|
|
UpdateManagementLevelGroupSchema,
|
|
CreateManagementLevelSchema,
|
|
UpdateManagementLevelSchema,
|
|
} from "../schemas/management-level.schema.js";
|
|
import {
|
|
CreateUtilizationCategorySchema,
|
|
UpdateUtilizationCategorySchema,
|
|
} from "../schemas/utilization-category.schema.js";
|
|
import {
|
|
CreateHolidayCalendarSchema,
|
|
UpdateHolidayCalendarSchema,
|
|
CreateHolidayCalendarEntrySchema,
|
|
UpdateHolidayCalendarEntrySchema,
|
|
PreviewResolvedHolidaysSchema,
|
|
} from "../schemas/holiday-calendar.schema.js";
|
|
import {
|
|
CreateCalculationRuleSchema,
|
|
UpdateCalculationRuleSchema,
|
|
} from "../schemas/calculation-rules.schema.js";
|
|
import { BlueprintTarget, FieldType } from "../types/enums.js";
|
|
|
|
// ─── BlueprintFieldDefinitionSchema ─────────────────────────────────────────
|
|
|
|
describe("BlueprintFieldDefinitionSchema", () => {
|
|
const validField = {
|
|
id: "field-1",
|
|
label: "Project Name",
|
|
key: "project_name",
|
|
type: FieldType.TEXT,
|
|
order: 0,
|
|
};
|
|
|
|
it("accepts a minimal valid field definition", () => {
|
|
const result = BlueprintFieldDefinitionSchema.parse(validField);
|
|
expect(result.id).toBe("field-1");
|
|
expect(result.required).toBe(false);
|
|
});
|
|
|
|
it("accepts a full field with options and validation", () => {
|
|
const result = BlueprintFieldDefinitionSchema.parse({
|
|
...validField,
|
|
required: true,
|
|
description: "The name of the project",
|
|
placeholder: "Enter name…",
|
|
defaultValue: "Untitled",
|
|
options: [{ value: "a", label: "Option A", color: "#ff0000" }],
|
|
validation: { minLength: 2, maxLength: 50 },
|
|
group: "General",
|
|
});
|
|
expect(result.required).toBe(true);
|
|
expect(result.options).toHaveLength(1);
|
|
expect(result.validation?.maxLength).toBe(50);
|
|
expect(result.group).toBe("General");
|
|
});
|
|
|
|
it("rejects key that does not match snake_case pattern", () => {
|
|
expect(() =>
|
|
BlueprintFieldDefinitionSchema.parse({ ...validField, key: "Project-Name" }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects key starting with a digit", () => {
|
|
expect(() =>
|
|
BlueprintFieldDefinitionSchema.parse({ ...validField, key: "1bad_key" }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects negative order value", () => {
|
|
expect(() => BlueprintFieldDefinitionSchema.parse({ ...validField, order: -1 })).toThrow();
|
|
});
|
|
|
|
it("rejects label longer than 200 characters", () => {
|
|
expect(() =>
|
|
BlueprintFieldDefinitionSchema.parse({ ...validField, label: "x".repeat(201) }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateBlueprintSchema ───────────────────────────────────────────────────
|
|
|
|
describe("CreateBlueprintSchema", () => {
|
|
it("accepts minimal valid input and applies defaults", () => {
|
|
const result = CreateBlueprintSchema.parse({
|
|
name: "Project Template",
|
|
target: BlueprintTarget.PROJECT,
|
|
});
|
|
expect(result.name).toBe("Project Template");
|
|
expect(result.target).toBe("PROJECT");
|
|
expect(result.fieldDefs).toEqual([]);
|
|
expect(result.defaults).toEqual({});
|
|
expect(result.validationRules).toEqual([]);
|
|
expect(result.description).toBeUndefined();
|
|
});
|
|
|
|
it("accepts full input with fieldDefs and validationRules", () => {
|
|
const result = CreateBlueprintSchema.parse({
|
|
name: "Resource Blueprint",
|
|
target: BlueprintTarget.RESOURCE,
|
|
description: "Used for all 3D artists",
|
|
fieldDefs: [
|
|
{
|
|
id: "f1",
|
|
label: "Skill Level",
|
|
key: "skill_level",
|
|
type: FieldType.SELECT,
|
|
order: 0,
|
|
options: [
|
|
{ value: "junior", label: "Junior" },
|
|
{ value: "senior", label: "Senior" },
|
|
],
|
|
},
|
|
],
|
|
defaults: { skill_level: "junior" },
|
|
validationRules: [{ field: "skill_level", rule: "required_if", params: { other: "active" } }],
|
|
});
|
|
expect(result.fieldDefs).toHaveLength(1);
|
|
expect(result.defaults.skill_level).toBe("junior");
|
|
expect(result.validationRules[0]?.rule).toBe("required_if");
|
|
});
|
|
|
|
it("rejects empty name", () => {
|
|
expect(() =>
|
|
CreateBlueprintSchema.parse({ name: "", target: BlueprintTarget.PROJECT }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects name longer than 200 characters", () => {
|
|
expect(() =>
|
|
CreateBlueprintSchema.parse({ name: "x".repeat(201), target: BlueprintTarget.PROJECT }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects invalid BlueprintTarget enum value", () => {
|
|
expect(() => CreateBlueprintSchema.parse({ name: "Test", target: "TEAM" })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateBlueprintSchema ───────────────────────────────────────────────────
|
|
|
|
describe("UpdateBlueprintSchema", () => {
|
|
it("accepts an empty object (all fields optional)", () => {
|
|
const result = UpdateBlueprintSchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.target).toBeUndefined();
|
|
});
|
|
|
|
it("accepts a partial update with only name", () => {
|
|
const result = UpdateBlueprintSchema.parse({ name: "Updated Name" });
|
|
expect(result.name).toBe("Updated Name");
|
|
expect(result.fieldDefs).toBeUndefined();
|
|
});
|
|
|
|
it("accepts a partial update with only target", () => {
|
|
const result = UpdateBlueprintSchema.parse({ target: BlueprintTarget.RESOURCE });
|
|
expect(result.target).toBe("RESOURCE");
|
|
});
|
|
|
|
it("rejects invalid target even in partial update", () => {
|
|
expect(() => UpdateBlueprintSchema.parse({ target: "INVALID" })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── generateDynamicZodSchema ────────────────────────────────────────────────
|
|
|
|
describe("generateDynamicZodSchema", () => {
|
|
it("generates a TEXT field as optional string by default", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{ id: "f1", label: "Title", key: "title", type: FieldType.TEXT, order: 0, required: false },
|
|
]);
|
|
const result = schema.parse({});
|
|
expect(result.title).toBeUndefined();
|
|
const withValue = schema.parse({ title: "Hello" });
|
|
expect(withValue.title).toBe("Hello");
|
|
});
|
|
|
|
it("generates a TEXT field as required when required=true", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{ id: "f1", label: "Title", key: "title", type: FieldType.TEXT, order: 0, required: true },
|
|
]);
|
|
expect(() => schema.parse({})).toThrow();
|
|
expect(schema.parse({ title: "Hello" }).title).toBe("Hello");
|
|
});
|
|
|
|
it("generates a NUMBER field with min/max validation", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f2",
|
|
label: "Rating",
|
|
key: "rating",
|
|
type: FieldType.NUMBER,
|
|
order: 0,
|
|
required: true,
|
|
validation: { min: 1, max: 10 },
|
|
},
|
|
]);
|
|
expect(schema.parse({ rating: 5 }).rating).toBe(5);
|
|
expect(() => schema.parse({ rating: 0 })).toThrow();
|
|
expect(() => schema.parse({ rating: 11 })).toThrow();
|
|
});
|
|
|
|
it("generates a BOOLEAN field", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f3",
|
|
label: "Active",
|
|
key: "is_active",
|
|
type: FieldType.BOOLEAN,
|
|
order: 0,
|
|
required: true,
|
|
},
|
|
]);
|
|
expect(schema.parse({ is_active: true }).is_active).toBe(true);
|
|
expect(schema.parse({ is_active: false }).is_active).toBe(false);
|
|
expect(() => schema.parse({ is_active: "yes" })).toThrow();
|
|
});
|
|
|
|
it("generates a DATE field using coerce", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f4",
|
|
label: "Start Date",
|
|
key: "start_date",
|
|
type: FieldType.DATE,
|
|
order: 0,
|
|
required: true,
|
|
},
|
|
]);
|
|
const result = schema.parse({ start_date: "2026-01-15" });
|
|
expect(result.start_date).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("generates a SELECT field restricted to defined option values", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f5",
|
|
label: "Status",
|
|
key: "status",
|
|
type: FieldType.SELECT,
|
|
order: 0,
|
|
required: true,
|
|
options: [
|
|
{ value: "draft", label: "Draft" },
|
|
{ value: "active", label: "Active" },
|
|
],
|
|
},
|
|
]);
|
|
expect(schema.parse({ status: "draft" }).status).toBe("draft");
|
|
expect(() => schema.parse({ status: "unknown" })).toThrow();
|
|
});
|
|
|
|
it("generates a MULTI_SELECT field as an array of valid enum values", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f6",
|
|
label: "Tags",
|
|
key: "tags",
|
|
type: FieldType.MULTI_SELECT,
|
|
order: 0,
|
|
required: true,
|
|
options: [
|
|
{ value: "cg", label: "CG" },
|
|
{ value: "vfx", label: "VFX" },
|
|
],
|
|
},
|
|
]);
|
|
expect(schema.parse({ tags: ["cg", "vfx"] }).tags).toEqual(["cg", "vfx"]);
|
|
expect(() => schema.parse({ tags: ["cg", "invalid"] })).toThrow();
|
|
});
|
|
|
|
it("generates an EMAIL field that validates email format", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{ id: "f7", label: "Email", key: "email", type: FieldType.EMAIL, order: 0, required: true },
|
|
]);
|
|
expect(schema.parse({ email: "user@example.com" }).email).toBe("user@example.com");
|
|
expect(() => schema.parse({ email: "not-an-email" })).toThrow();
|
|
});
|
|
|
|
it("generates a URL field that validates URL format", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{ id: "f8", label: "Website", key: "website", type: FieldType.URL, order: 0, required: true },
|
|
]);
|
|
expect(schema.parse({ website: "https://example.com" }).website).toBe("https://example.com");
|
|
expect(() => schema.parse({ website: "not-a-url" })).toThrow();
|
|
});
|
|
|
|
it("generates a TEXT field with minLength/maxLength validation", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{
|
|
id: "f9",
|
|
label: "Code",
|
|
key: "code",
|
|
type: FieldType.TEXT,
|
|
order: 0,
|
|
required: true,
|
|
validation: { minLength: 3, maxLength: 8 },
|
|
},
|
|
]);
|
|
expect(schema.parse({ code: "ABC" }).code).toBe("ABC");
|
|
expect(() => schema.parse({ code: "AB" })).toThrow();
|
|
expect(() => schema.parse({ code: "TOOLONGCODE" })).toThrow();
|
|
});
|
|
|
|
it("handles a SELECT field with no options as a plain string", () => {
|
|
const schema = generateDynamicZodSchema([
|
|
{ id: "f10", label: "Tag", key: "tag", type: FieldType.SELECT, order: 0, required: true },
|
|
]);
|
|
expect(schema.parse({ tag: "anything" }).tag).toBe("anything");
|
|
});
|
|
});
|
|
|
|
// ─── CreateOrgUnitSchema ─────────────────────────────────────────────────────
|
|
|
|
describe("CreateOrgUnitSchema", () => {
|
|
it("accepts valid minimal input with defaults", () => {
|
|
const result = CreateOrgUnitSchema.parse({ name: "Studio Munich", level: 5 });
|
|
expect(result.name).toBe("Studio Munich");
|
|
expect(result.level).toBe(5);
|
|
expect(result.sortOrder).toBe(0);
|
|
expect(result.shortName).toBeUndefined();
|
|
expect(result.parentId).toBeUndefined();
|
|
});
|
|
|
|
it("accepts full input with all optional fields", () => {
|
|
const result = CreateOrgUnitSchema.parse({
|
|
name: "Department VFX",
|
|
shortName: "VFX",
|
|
level: 6,
|
|
parentId: "parent-1",
|
|
sortOrder: 3,
|
|
});
|
|
expect(result.shortName).toBe("VFX");
|
|
expect(result.parentId).toBe("parent-1");
|
|
expect(result.sortOrder).toBe(3);
|
|
});
|
|
|
|
it("rejects level below 5", () => {
|
|
expect(() => CreateOrgUnitSchema.parse({ name: "Test", level: 4 })).toThrow();
|
|
});
|
|
|
|
it("rejects level above 7", () => {
|
|
expect(() => CreateOrgUnitSchema.parse({ name: "Test", level: 8 })).toThrow();
|
|
});
|
|
|
|
it("rejects shortName longer than 50 characters", () => {
|
|
expect(() =>
|
|
CreateOrgUnitSchema.parse({ name: "Test", level: 5, shortName: "x".repeat(51) }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateOrgUnitSchema ─────────────────────────────────────────────────────
|
|
|
|
describe("UpdateOrgUnitSchema", () => {
|
|
it("accepts empty object (all optional)", () => {
|
|
const result = UpdateOrgUnitSchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.isActive).toBeUndefined();
|
|
});
|
|
|
|
it("accepts nullable shortName to clear the field", () => {
|
|
const result = UpdateOrgUnitSchema.parse({ shortName: null });
|
|
expect(result.shortName).toBeNull();
|
|
});
|
|
|
|
it("accepts nullable parentId to unset parent", () => {
|
|
const result = UpdateOrgUnitSchema.parse({ parentId: null });
|
|
expect(result.parentId).toBeNull();
|
|
});
|
|
|
|
it("accepts isActive flag", () => {
|
|
const result = UpdateOrgUnitSchema.parse({ isActive: false });
|
|
expect(result.isActive).toBe(false);
|
|
});
|
|
|
|
it("rejects name longer than 200 characters", () => {
|
|
expect(() => UpdateOrgUnitSchema.parse({ name: "x".repeat(201) })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateManagementLevelGroupSchema ───────────────────────────────────────
|
|
|
|
describe("CreateManagementLevelGroupSchema", () => {
|
|
it("accepts valid input with defaults", () => {
|
|
const result = CreateManagementLevelGroupSchema.parse({
|
|
name: "Senior Management",
|
|
targetPercentage: 0.15,
|
|
});
|
|
expect(result.name).toBe("Senior Management");
|
|
expect(result.targetPercentage).toBe(0.15);
|
|
expect(result.sortOrder).toBe(0);
|
|
});
|
|
|
|
it("accepts boundary values for targetPercentage (0 and 1)", () => {
|
|
expect(
|
|
CreateManagementLevelGroupSchema.parse({ name: "A", targetPercentage: 0 }).targetPercentage,
|
|
).toBe(0);
|
|
expect(
|
|
CreateManagementLevelGroupSchema.parse({ name: "B", targetPercentage: 1 }).targetPercentage,
|
|
).toBe(1);
|
|
});
|
|
|
|
it("rejects targetPercentage above 1", () => {
|
|
expect(() =>
|
|
CreateManagementLevelGroupSchema.parse({ name: "Test", targetPercentage: 1.01 }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects targetPercentage below 0", () => {
|
|
expect(() =>
|
|
CreateManagementLevelGroupSchema.parse({ name: "Test", targetPercentage: -0.01 }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects empty name", () => {
|
|
expect(() =>
|
|
CreateManagementLevelGroupSchema.parse({ name: "", targetPercentage: 0.1 }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateManagementLevelGroupSchema ───────────────────────────────────────
|
|
|
|
describe("UpdateManagementLevelGroupSchema", () => {
|
|
it("accepts empty object", () => {
|
|
const result = UpdateManagementLevelGroupSchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.targetPercentage).toBeUndefined();
|
|
});
|
|
|
|
it("accepts partial update with only sortOrder", () => {
|
|
const result = UpdateManagementLevelGroupSchema.parse({ sortOrder: 5 });
|
|
expect(result.sortOrder).toBe(5);
|
|
expect(result.name).toBeUndefined();
|
|
});
|
|
|
|
it("rejects targetPercentage out of range in partial update", () => {
|
|
expect(() => UpdateManagementLevelGroupSchema.parse({ targetPercentage: 2 })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateManagementLevelSchema ────────────────────────────────────────────
|
|
|
|
describe("CreateManagementLevelSchema", () => {
|
|
it("accepts valid input", () => {
|
|
const result = CreateManagementLevelSchema.parse({ name: "Director", groupId: "grp-1" });
|
|
expect(result.name).toBe("Director");
|
|
expect(result.groupId).toBe("grp-1");
|
|
});
|
|
|
|
it("rejects empty name", () => {
|
|
expect(() => CreateManagementLevelSchema.parse({ name: "", groupId: "grp-1" })).toThrow();
|
|
});
|
|
|
|
it("rejects missing groupId", () => {
|
|
expect(() => CreateManagementLevelSchema.parse({ name: "Director" })).toThrow();
|
|
});
|
|
|
|
it("rejects name longer than 100 characters", () => {
|
|
expect(() =>
|
|
CreateManagementLevelSchema.parse({ name: "x".repeat(101), groupId: "grp-1" }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateManagementLevelSchema ────────────────────────────────────────────
|
|
|
|
describe("UpdateManagementLevelSchema", () => {
|
|
it("accepts empty object", () => {
|
|
const result = UpdateManagementLevelSchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.groupId).toBeUndefined();
|
|
});
|
|
|
|
it("accepts partial update with only groupId", () => {
|
|
const result = UpdateManagementLevelSchema.parse({ groupId: "grp-2" });
|
|
expect(result.groupId).toBe("grp-2");
|
|
});
|
|
|
|
it("rejects empty name in partial update", () => {
|
|
expect(() => UpdateManagementLevelSchema.parse({ name: "" })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateUtilizationCategorySchema ────────────────────────────────────────
|
|
|
|
describe("CreateUtilizationCategorySchema", () => {
|
|
it("accepts minimal valid input with defaults", () => {
|
|
const result = CreateUtilizationCategorySchema.parse({ code: "PROD", name: "Production" });
|
|
expect(result.code).toBe("PROD");
|
|
expect(result.name).toBe("Production");
|
|
expect(result.sortOrder).toBe(0);
|
|
expect(result.isDefault).toBe(false);
|
|
expect(result.description).toBeUndefined();
|
|
});
|
|
|
|
it("accepts full input including description", () => {
|
|
const result = CreateUtilizationCategorySchema.parse({
|
|
code: "SICK",
|
|
name: "Sick Leave",
|
|
description: "Covers illness absence",
|
|
sortOrder: 2,
|
|
isDefault: true,
|
|
});
|
|
expect(result.isDefault).toBe(true);
|
|
expect(result.description).toBe("Covers illness absence");
|
|
});
|
|
|
|
it("rejects code longer than 20 characters", () => {
|
|
expect(() =>
|
|
CreateUtilizationCategorySchema.parse({ code: "x".repeat(21), name: "Test" }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects description longer than 500 characters", () => {
|
|
expect(() =>
|
|
CreateUtilizationCategorySchema.parse({
|
|
code: "ABC",
|
|
name: "Test",
|
|
description: "x".repeat(501),
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects empty code", () => {
|
|
expect(() => CreateUtilizationCategorySchema.parse({ code: "", name: "Test" })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateUtilizationCategorySchema ────────────────────────────────────────
|
|
|
|
describe("UpdateUtilizationCategorySchema", () => {
|
|
it("accepts empty object", () => {
|
|
const result = UpdateUtilizationCategorySchema.parse({});
|
|
expect(result.code).toBeUndefined();
|
|
expect(result.isDefault).toBeUndefined();
|
|
});
|
|
|
|
it("accepts nullable description to clear the field", () => {
|
|
const result = UpdateUtilizationCategorySchema.parse({ description: null });
|
|
expect(result.description).toBeNull();
|
|
});
|
|
|
|
it("accepts isActive flag", () => {
|
|
const result = UpdateUtilizationCategorySchema.parse({ isActive: false });
|
|
expect(result.isActive).toBe(false);
|
|
});
|
|
|
|
it("rejects code longer than 20 characters in partial update", () => {
|
|
expect(() => UpdateUtilizationCategorySchema.parse({ code: "x".repeat(21) })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateHolidayCalendarSchema ─────────────────────────────────────────────
|
|
|
|
describe("CreateHolidayCalendarSchema", () => {
|
|
it("accepts valid COUNTRY-scope calendar", () => {
|
|
const result = CreateHolidayCalendarSchema.parse({
|
|
name: "Germany Public Holidays",
|
|
scopeType: "COUNTRY",
|
|
countryId: "DE",
|
|
});
|
|
expect(result.name).toBe("Germany Public Holidays");
|
|
expect(result.scopeType).toBe("COUNTRY");
|
|
expect(result.stateCode).toBeUndefined();
|
|
expect(result.priority).toBeUndefined();
|
|
});
|
|
|
|
it("accepts valid STATE-scope calendar with stateCode", () => {
|
|
const result = CreateHolidayCalendarSchema.parse({
|
|
name: "Bavaria Holidays",
|
|
scopeType: "STATE",
|
|
countryId: "DE",
|
|
stateCode: "BY",
|
|
priority: 10,
|
|
isActive: true,
|
|
});
|
|
expect(result.stateCode).toBe("BY");
|
|
expect(result.priority).toBe(10);
|
|
});
|
|
|
|
it("rejects invalid scopeType", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarSchema.parse({ name: "Test", scopeType: "REGION", countryId: "DE" }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects priority below -100", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarSchema.parse({
|
|
name: "Test",
|
|
scopeType: "COUNTRY",
|
|
countryId: "DE",
|
|
priority: -101,
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects priority above 100", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarSchema.parse({
|
|
name: "Test",
|
|
scopeType: "COUNTRY",
|
|
countryId: "DE",
|
|
priority: 101,
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects stateCode longer than 16 characters", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarSchema.parse({
|
|
name: "Test",
|
|
scopeType: "STATE",
|
|
countryId: "DE",
|
|
stateCode: "x".repeat(17),
|
|
}),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateHolidayCalendarSchema ─────────────────────────────────────────────
|
|
|
|
describe("UpdateHolidayCalendarSchema", () => {
|
|
it("accepts empty object", () => {
|
|
const result = UpdateHolidayCalendarSchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.stateCode).toBeUndefined();
|
|
});
|
|
|
|
it("accepts nullable stateCode to clear it", () => {
|
|
const result = UpdateHolidayCalendarSchema.parse({ stateCode: null });
|
|
expect(result.stateCode).toBeNull();
|
|
});
|
|
|
|
it("accepts partial update with name and isActive", () => {
|
|
const result = UpdateHolidayCalendarSchema.parse({ name: "Updated Calendar", isActive: false });
|
|
expect(result.name).toBe("Updated Calendar");
|
|
expect(result.isActive).toBe(false);
|
|
});
|
|
|
|
it("rejects name longer than 120 characters", () => {
|
|
expect(() => UpdateHolidayCalendarSchema.parse({ name: "x".repeat(121) })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateHolidayCalendarEntrySchema ────────────────────────────────────────
|
|
|
|
describe("CreateHolidayCalendarEntrySchema", () => {
|
|
it("accepts valid entry with date coercion from string", () => {
|
|
const result = CreateHolidayCalendarEntrySchema.parse({
|
|
holidayCalendarId: "cal-1",
|
|
date: "2026-12-25",
|
|
name: "Christmas Day",
|
|
});
|
|
expect(result.holidayCalendarId).toBe("cal-1");
|
|
expect(result.date).toBeInstanceOf(Date);
|
|
expect(result.name).toBe("Christmas Day");
|
|
expect(result.isRecurringAnnual).toBeUndefined();
|
|
});
|
|
|
|
it("accepts full entry with all optional fields", () => {
|
|
const result = CreateHolidayCalendarEntrySchema.parse({
|
|
holidayCalendarId: "cal-1",
|
|
date: "2026-10-03",
|
|
name: "German Unity Day",
|
|
isRecurringAnnual: true,
|
|
source: "Official Federal Calendar",
|
|
});
|
|
expect(result.isRecurringAnnual).toBe(true);
|
|
expect(result.source).toBe("Official Federal Calendar");
|
|
});
|
|
|
|
it("rejects empty name", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarEntrySchema.parse({
|
|
holidayCalendarId: "cal-1",
|
|
date: "2026-01-01",
|
|
name: "",
|
|
}),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects missing holidayCalendarId", () => {
|
|
expect(() =>
|
|
CreateHolidayCalendarEntrySchema.parse({ date: "2026-01-01", name: "New Year" }),
|
|
).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateHolidayCalendarEntrySchema ────────────────────────────────────────
|
|
|
|
describe("UpdateHolidayCalendarEntrySchema", () => {
|
|
it("accepts empty object", () => {
|
|
const result = UpdateHolidayCalendarEntrySchema.parse({});
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.date).toBeUndefined();
|
|
});
|
|
|
|
it("accepts partial update with date coercion", () => {
|
|
const result = UpdateHolidayCalendarEntrySchema.parse({ date: "2027-01-01" });
|
|
expect(result.date).toBeInstanceOf(Date);
|
|
});
|
|
|
|
it("accepts nullable source to clear it", () => {
|
|
const result = UpdateHolidayCalendarEntrySchema.parse({ source: null });
|
|
expect(result.source).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ─── PreviewResolvedHolidaysSchema ───────────────────────────────────────────
|
|
|
|
describe("PreviewResolvedHolidaysSchema", () => {
|
|
it("accepts valid input with required fields", () => {
|
|
const result = PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2026 });
|
|
expect(result.countryId).toBe("DE");
|
|
expect(result.year).toBe(2026);
|
|
expect(result.stateCode).toBeUndefined();
|
|
});
|
|
|
|
it("accepts input with optional stateCode and metroCityId", () => {
|
|
const result = PreviewResolvedHolidaysSchema.parse({
|
|
countryId: "DE",
|
|
stateCode: "BY",
|
|
metroCityId: "city-1",
|
|
year: 2026,
|
|
});
|
|
expect(result.stateCode).toBe("BY");
|
|
expect(result.metroCityId).toBe("city-1");
|
|
});
|
|
|
|
it("rejects year below 2000", () => {
|
|
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 1999 })).toThrow();
|
|
});
|
|
|
|
it("rejects year above 2100", () => {
|
|
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2101 })).toThrow();
|
|
});
|
|
|
|
it("rejects non-integer year", () => {
|
|
expect(() => PreviewResolvedHolidaysSchema.parse({ countryId: "DE", year: 2026.5 })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── CreateCalculationRuleSchema ─────────────────────────────────────────────
|
|
|
|
describe("CreateCalculationRuleSchema", () => {
|
|
const validRule = {
|
|
name: "Public Holiday Charge Rule",
|
|
triggerType: "PUBLIC_HOLIDAY",
|
|
costEffect: "CHARGE",
|
|
chargeabilityEffect: "COUNT",
|
|
};
|
|
|
|
it("accepts minimal valid input with defaults", () => {
|
|
const result = CreateCalculationRuleSchema.parse(validRule);
|
|
expect(result.name).toBe("Public Holiday Charge Rule");
|
|
expect(result.triggerType).toBe("PUBLIC_HOLIDAY");
|
|
expect(result.costEffect).toBe("CHARGE");
|
|
expect(result.chargeabilityEffect).toBe("COUNT");
|
|
expect(result.priority).toBe(0);
|
|
expect(result.isActive).toBe(true);
|
|
expect(result.description).toBeUndefined();
|
|
});
|
|
|
|
it("accepts full input with all optional fields", () => {
|
|
const result = CreateCalculationRuleSchema.parse({
|
|
...validRule,
|
|
description: "Charges client on public holidays",
|
|
triggerType: "SICK",
|
|
costEffect: "REDUCE",
|
|
costReductionPercent: 50,
|
|
projectId: "proj-1",
|
|
orderType: "CHARGEABLE",
|
|
chargeabilityEffect: "SKIP",
|
|
priority: 100,
|
|
isActive: false,
|
|
});
|
|
expect(result.costReductionPercent).toBe(50);
|
|
expect(result.triggerType).toBe("SICK");
|
|
expect(result.chargeabilityEffect).toBe("SKIP");
|
|
expect(result.isActive).toBe(false);
|
|
});
|
|
|
|
it("rejects invalid triggerType", () => {
|
|
expect(() =>
|
|
CreateCalculationRuleSchema.parse({ ...validRule, triggerType: "OVERTIME" }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects costReductionPercent above 100", () => {
|
|
expect(() =>
|
|
CreateCalculationRuleSchema.parse({ ...validRule, costReductionPercent: 101 }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("rejects priority above 1000", () => {
|
|
expect(() => CreateCalculationRuleSchema.parse({ ...validRule, priority: 1001 })).toThrow();
|
|
});
|
|
|
|
it("rejects priority below 0", () => {
|
|
expect(() => CreateCalculationRuleSchema.parse({ ...validRule, priority: -1 })).toThrow();
|
|
});
|
|
});
|
|
|
|
// ─── UpdateCalculationRuleSchema ─────────────────────────────────────────────
|
|
|
|
describe("UpdateCalculationRuleSchema", () => {
|
|
it("accepts minimal input with only required id", () => {
|
|
const result = UpdateCalculationRuleSchema.parse({ id: "rule-1" });
|
|
expect(result.id).toBe("rule-1");
|
|
expect(result.name).toBeUndefined();
|
|
expect(result.triggerType).toBeUndefined();
|
|
});
|
|
|
|
it("accepts partial update with name and priority", () => {
|
|
const result = UpdateCalculationRuleSchema.parse({
|
|
id: "rule-1",
|
|
name: "Updated Rule",
|
|
priority: 5,
|
|
});
|
|
expect(result.name).toBe("Updated Rule");
|
|
expect(result.priority).toBe(5);
|
|
});
|
|
|
|
it("requires id to be present", () => {
|
|
expect(() => UpdateCalculationRuleSchema.parse({ name: "Some Rule" })).toThrow();
|
|
});
|
|
|
|
it("rejects invalid costEffect in partial update", () => {
|
|
expect(() =>
|
|
UpdateCalculationRuleSchema.parse({ id: "rule-1", costEffect: "IGNORE" }),
|
|
).toThrow();
|
|
});
|
|
});
|