test(shared): add 215 schema validation tests covering all 17 Zod schemas

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>
This commit is contained in:
2026-04-10 15:47:53 +02:00
parent dfeb4d361e
commit d0926601ea
3 changed files with 1839 additions and 0 deletions
@@ -0,0 +1,467 @@
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 (15)", () => {
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 15 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 0100", () => {
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();
});
});