Files
CapaKraken/packages/shared/src/__tests__/schema-resource-client-role.test.ts
T
Hartmut d0926601ea 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>
2026-04-10 15:47:53 +02:00

468 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});