import { describe, expect, it } from "vitest"; import { WeekdayAvailabilitySchema, SkillEntrySchema, CreateResourceSchema, UpdateResourceSchema, } from "../schemas/resource.schema.js"; import { CreateClientSchema, UpdateClientSchema } from "../schemas/client.schema.js"; import { CreateRoleSchema, UpdateRoleSchema, ResourceRoleSchema } from "../schemas/role.schema.js"; import { ResourceType } from "../types/enums.js"; // ─── WeekdayAvailabilitySchema ─────────────────────────────────────────────── describe("WeekdayAvailabilitySchema", () => { it("accepts valid weekday-only input", () => { const result = WeekdayAvailabilitySchema.parse({ monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }); expect(result.monday).toBe(8); expect(result.saturday).toBeUndefined(); expect(result.sunday).toBeUndefined(); }); it("accepts full input including weekend hours", () => { const result = WeekdayAvailabilitySchema.parse({ monday: 6, tuesday: 6, wednesday: 6, thursday: 6, friday: 6, saturday: 4, sunday: 0, }); expect(result.saturday).toBe(4); expect(result.sunday).toBe(0); }); it("accepts boundary values (0 and 24)", () => { const result = WeekdayAvailabilitySchema.parse({ monday: 0, tuesday: 24, wednesday: 0, thursday: 24, friday: 0, }); expect(result.monday).toBe(0); expect(result.tuesday).toBe(24); }); it("rejects hours above 24", () => { expect(() => WeekdayAvailabilitySchema.parse({ monday: 25, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }), ).toThrow(); }); it("rejects hours below 0", () => { expect(() => WeekdayAvailabilitySchema.parse({ monday: -1, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }), ).toThrow(); }); }); // ─── SkillEntrySchema ──────────────────────────────────────────────────────── describe("SkillEntrySchema", () => { it("accepts minimal valid skill entry", () => { const result = SkillEntrySchema.parse({ skill: "Houdini", proficiency: 3 }); expect(result.skill).toBe("Houdini"); expect(result.proficiency).toBe(3); expect(result.category).toBeUndefined(); expect(result.certified).toBeUndefined(); }); it("accepts full skill entry with all optional fields", () => { const result = SkillEntrySchema.parse({ skill: "Maya", category: "3D Modeling", proficiency: 5, yearsExperience: 7, certified: true, isMainSkill: true, }); expect(result.category).toBe("3D Modeling"); expect(result.yearsExperience).toBe(7); expect(result.certified).toBe(true); expect(result.isMainSkill).toBe(true); }); it("accepts all valid proficiency literals (1–5)", () => { for (const level of [1, 2, 3, 4, 5] as const) { const result = SkillEntrySchema.parse({ skill: "Test", proficiency: level }); expect(result.proficiency).toBe(level); } }); it("rejects proficiency outside the 1–5 range", () => { expect(() => SkillEntrySchema.parse({ skill: "Test", proficiency: 0 })).toThrow(); expect(() => SkillEntrySchema.parse({ skill: "Test", proficiency: 6 })).toThrow(); }); it("rejects empty skill name", () => { expect(() => SkillEntrySchema.parse({ skill: "", proficiency: 3 })).toThrow(); }); it("rejects yearsExperience above 50", () => { expect(() => SkillEntrySchema.parse({ skill: "Nuke", proficiency: 2, yearsExperience: 51 }), ).toThrow(); }); }); // ─── CreateResourceSchema ──────────────────────────────────────────────────── describe("CreateResourceSchema", () => { const minimalResource = { eid: "EMP-001", displayName: "Jane Smith", email: "jane.smith@example.com", lcrCents: 6000, ucrCents: 4000, }; it("accepts minimal valid resource and applies defaults", () => { const result = CreateResourceSchema.parse(minimalResource); expect(result.eid).toBe("EMP-001"); expect(result.currency).toBe("EUR"); expect(result.chargeabilityTarget).toBe(80); expect(result.availability).toEqual({ monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8, }); expect(result.skills).toEqual([]); expect(result.dynamicFields).toEqual({}); }); it("accepts full resource with all optional fields populated", () => { const result = CreateResourceSchema.parse({ ...minimalResource, chapter: "3D Animation", currency: "USD", chargeabilityTarget: 75, availability: { monday: 6, tuesday: 6, wednesday: 6, thursday: 6, friday: 6 }, skills: [{ skill: "Maya", proficiency: 4 }], dynamicFields: { department: "VFX" }, blueprintId: "bp-1", portfolioUrl: "https://portfolio.example.com", roleId: "role-42", postalCode: "80331", federalState: "BY", countryId: "country-de", metroCityId: "metro-muc", orgUnitId: "ou-1", managementLevelGroupId: "mlg-1", managementLevelId: "ml-2", resourceType: ResourceType.EMPLOYEE, chgResponsibility: false, rolledOff: false, departed: false, enterpriseId: "ENT-999", clientUnitId: "cu-5", fte: 1, }); expect(result.chapter).toBe("3D Animation"); expect(result.currency).toBe("USD"); expect(result.resourceType).toBe(ResourceType.EMPLOYEE); expect(result.skills).toHaveLength(1); expect(result.fte).toBe(1); }); it("applies default availability when omitted", () => { const result = CreateResourceSchema.parse(minimalResource); const days = ["monday", "tuesday", "wednesday", "thursday", "friday"] as const; for (const day of days) { expect(result.availability[day]).toBe(8); } }); it("rejects invalid email address", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, email: "not-an-email" }), ).toThrow(); }); it("rejects eid longer than 50 characters", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, eid: "X".repeat(51) })).toThrow(); }); it("rejects negative lcrCents", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, lcrCents: -1 })).toThrow(); }); it("rejects non-integer rate values", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, lcrCents: 60.5 })).toThrow(); }); it("rejects chargeabilityTarget outside 0–100", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, chargeabilityTarget: 101 }), ).toThrow(); expect(() => CreateResourceSchema.parse({ ...minimalResource, chargeabilityTarget: -1 }), ).toThrow(); }); it("rejects currency not exactly 3 characters", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, currency: "EURO" })).toThrow(); expect(() => CreateResourceSchema.parse({ ...minimalResource, currency: "EU" })).toThrow(); }); it("rejects invalid resourceType enum value", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, resourceType: "CONSULTANT" }), ).toThrow(); }); it("rejects fte below 0.01 or above 1", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, fte: 0 })).toThrow(); expect(() => CreateResourceSchema.parse({ ...minimalResource, fte: 1.01 })).toThrow(); }); it("accepts empty string as portfolioUrl", () => { const result = CreateResourceSchema.parse({ ...minimalResource, portfolioUrl: "", }); expect(result.portfolioUrl).toBe(""); }); it("rejects portfolioUrl that is neither a valid URL nor empty string", () => { expect(() => CreateResourceSchema.parse({ ...minimalResource, portfolioUrl: "not-a-url" }), ).toThrow(); }); it("accepts all ResourceType enum values", () => { const types = [ ResourceType.EMPLOYEE, ResourceType.FREELANCER, ResourceType.APPRENTICE, ResourceType.INTERN, ResourceType.STUDENT, ]; for (const resourceType of types) { const result = CreateResourceSchema.parse({ ...minimalResource, resourceType }); expect(result.resourceType).toBe(resourceType); } }); }); // ─── UpdateResourceSchema ──────────────────────────────────────────────────── describe("UpdateResourceSchema", () => { it("accepts empty object (all fields optional)", () => { const result = UpdateResourceSchema.parse({}); expect(result.eid).toBeUndefined(); expect(result.displayName).toBeUndefined(); expect(result.isActive).toBeUndefined(); }); it("accepts partial update with only displayName", () => { const result = UpdateResourceSchema.parse({ displayName: "Updated Name" }); expect(result.displayName).toBe("Updated Name"); expect(result.email).toBeUndefined(); }); it("accepts isActive flag not present in CreateResourceSchema", () => { const result = UpdateResourceSchema.parse({ isActive: false }); expect(result.isActive).toBe(false); }); it("still validates constrained fields when provided", () => { expect(() => UpdateResourceSchema.parse({ email: "bad-email" })).toThrow(); expect(() => UpdateResourceSchema.parse({ chargeabilityTarget: 200 })).toThrow(); }); }); // ─── CreateClientSchema ────────────────────────────────────────────────────── describe("CreateClientSchema", () => { it("accepts minimal valid client and applies defaults", () => { const result = CreateClientSchema.parse({ name: "Acme Corp" }); expect(result.name).toBe("Acme Corp"); expect(result.sortOrder).toBe(0); expect(result.code).toBeUndefined(); expect(result.parentId).toBeUndefined(); expect(result.tags).toBeUndefined(); }); it("accepts full client input with all optional fields", () => { const result = CreateClientSchema.parse({ name: "Global Media GmbH", code: "GM-001", parentId: "parent-42", sortOrder: 5, tags: ["vip", "premium"], }); expect(result.code).toBe("GM-001"); expect(result.parentId).toBe("parent-42"); expect(result.sortOrder).toBe(5); expect(result.tags).toEqual(["vip", "premium"]); }); it("rejects empty name", () => { expect(() => CreateClientSchema.parse({ name: "" })).toThrow(); }); it("rejects name longer than 300 characters", () => { expect(() => CreateClientSchema.parse({ name: "A".repeat(301) })).toThrow(); }); it("rejects tag exceeding 50 characters", () => { expect(() => CreateClientSchema.parse({ name: "Client", tags: ["X".repeat(51)] })).toThrow(); }); it("rejects non-integer sortOrder", () => { expect(() => CreateClientSchema.parse({ name: "Client", sortOrder: 1.5 })).toThrow(); }); it("rejects code longer than 50 characters", () => { expect(() => CreateClientSchema.parse({ name: "Client", code: "C".repeat(51) })).toThrow(); }); }); // ─── UpdateClientSchema ────────────────────────────────────────────────────── describe("UpdateClientSchema", () => { it("accepts empty update object (all fields optional)", () => { const result = UpdateClientSchema.parse({}); expect(result.name).toBeUndefined(); expect(result.isActive).toBeUndefined(); }); it("accepts nullable code (clearing the code)", () => { const result = UpdateClientSchema.parse({ code: null }); expect(result.code).toBeNull(); }); it("accepts nullable parentId (detaching from parent)", () => { const result = UpdateClientSchema.parse({ parentId: null }); expect(result.parentId).toBeNull(); }); it("accepts isActive flag", () => { const result = UpdateClientSchema.parse({ isActive: false }); expect(result.isActive).toBe(false); }); it("still validates name length when provided", () => { expect(() => UpdateClientSchema.parse({ name: "" })).toThrow(); expect(() => UpdateClientSchema.parse({ name: "N".repeat(301) })).toThrow(); }); }); // ─── CreateRoleSchema ──────────────────────────────────────────────────────── describe("CreateRoleSchema", () => { it("accepts minimal valid role (name only)", () => { const result = CreateRoleSchema.parse({ name: "3D Artist" }); expect(result.name).toBe("3D Artist"); expect(result.description).toBeUndefined(); expect(result.color).toBeUndefined(); }); it("accepts full role with description and color", () => { const result = CreateRoleSchema.parse({ name: "Lead Animator", description: "Responsible for animation direction and team leadership.", color: "#FF5733", }); expect(result.description).toBe("Responsible for animation direction and team leadership."); expect(result.color).toBe("#FF5733"); }); it("accepts lowercase hex color", () => { const result = CreateRoleSchema.parse({ name: "VFX Artist", color: "#a1b2c3" }); expect(result.color).toBe("#a1b2c3"); }); it("rejects empty name", () => { expect(() => CreateRoleSchema.parse({ name: "" })).toThrow(); }); it("rejects name longer than 100 characters", () => { expect(() => CreateRoleSchema.parse({ name: "R".repeat(101) })).toThrow(); }); it("rejects description longer than 500 characters", () => { expect(() => CreateRoleSchema.parse({ name: "Role", description: "D".repeat(501) })).toThrow(); }); it("rejects invalid hex color (missing hash)", () => { expect(() => CreateRoleSchema.parse({ name: "Role", color: "FF5733" })).toThrow(); }); it("rejects invalid hex color (wrong length)", () => { expect(() => CreateRoleSchema.parse({ name: "Role", color: "#FFF" })).toThrow(); expect(() => CreateRoleSchema.parse({ name: "Role", color: "#FF57330" })).toThrow(); }); }); // ─── UpdateRoleSchema ──────────────────────────────────────────────────────── describe("UpdateRoleSchema", () => { it("accepts empty update object (all fields optional)", () => { const result = UpdateRoleSchema.parse({}); expect(result.name).toBeUndefined(); expect(result.color).toBeUndefined(); expect(result.isActive).toBeUndefined(); }); it("accepts partial update with only name", () => { const result = UpdateRoleSchema.parse({ name: "Senior 3D Artist" }); expect(result.name).toBe("Senior 3D Artist"); }); it("accepts isActive flag not present in CreateRoleSchema", () => { const result = UpdateRoleSchema.parse({ isActive: true }); expect(result.isActive).toBe(true); }); it("still validates color format when provided", () => { expect(() => UpdateRoleSchema.parse({ color: "invalid-color" })).toThrow(); }); }); // ─── ResourceRoleSchema ────────────────────────────────────────────────────── describe("ResourceRoleSchema", () => { it("accepts minimal input and applies isPrimary default", () => { const result = ResourceRoleSchema.parse({ roleId: "role-1" }); expect(result.roleId).toBe("role-1"); expect(result.isPrimary).toBe(false); }); it("accepts explicit isPrimary true", () => { const result = ResourceRoleSchema.parse({ roleId: "role-99", isPrimary: true }); expect(result.isPrimary).toBe(true); }); it("rejects missing roleId", () => { expect(() => ResourceRoleSchema.parse({})).toThrow(); expect(() => ResourceRoleSchema.parse({ isPrimary: true })).toThrow(); }); it("rejects non-boolean isPrimary", () => { expect(() => ResourceRoleSchema.parse({ roleId: "role-1", isPrimary: "yes" })).toThrow(); }); });