/** * Tests for the zod-validation in PATCH /api/profile/me/demographics. * Replicates the schema from the endpoint to test the validation * boundaries (anti-Cascade-Bug: invalid input must 422, not 500). * * Updated for Demographics Phase D: employmentStatus/shiftWork/industry/jobTenure * replace profession. profession kept as legacy optional field. */ import { describe, expect, it } from "vitest"; import { z } from "zod"; const BUNDESLAND_REGEX = /^DE-(BW|BY|BE|BB|HB|HH|HE|MV|NI|NW|RP|SL|SN|ST|SH|TH)$/; const GENDER_VALUES = ["male", "female", "diverse", "no_answer"] as const; const MARITAL_VALUES = [ "single", "partnered", "married", "divorced", "widowed", "no_answer", ] as const; const EMPLOYMENT_STATUS_VALUES = [ "employed", "self_employed", "in_training", "unemployed", "retired", "homemaking", "other", ] as const; const JOB_TENURE_VALUES = [ "less_1y", "1_3y", "3_5y", "5_10y", "more_10y", ] as const; const Schema = z.object({ birthYear: z.number().int().min(1920).max(2010).nullable().optional(), gender: z.enum(GENDER_VALUES).nullable().optional(), maritalStatus: z.enum(MARITAL_VALUES).nullable().optional(), profession: z.string().trim().max(80).nullable().optional(), // legacy employmentStatus: z.enum(EMPLOYMENT_STATUS_VALUES).nullable().optional(), shiftWork: z.boolean().nullable().optional(), industry: z.string().trim().max(64).nullable().optional(), jobTenure: z.enum(JOB_TENURE_VALUES).nullable().optional(), bundesland: z.string().regex(BUNDESLAND_REGEX).nullable().optional(), city: z.string().trim().max(80).nullable().optional(), }); describe("demographics zod schema — happy", () => { it("accepts a complete valid demographic payload (new fields)", () => { const r = Schema.safeParse({ birthYear: 1989, gender: "diverse", maritalStatus: "partnered", employmentStatus: "employed", shiftWork: true, industry: "Gesundheit", jobTenure: "3_5y", bundesland: "DE-BY", city: "München", }); expect(r.success).toBe(true); }); it("accepts old payload with `profession` (backwards-compat)", () => { const r = Schema.safeParse({ birthYear: 1989, gender: "diverse", maritalStatus: "partnered", profession: "Pflege", bundesland: "DE-BY", city: "München", }); expect(r.success).toBe(true); }); it("accepts a partial subset (1 field at a time)", () => { expect(Schema.safeParse({ birthYear: 1989 }).success).toBe(true); expect(Schema.safeParse({ city: "Berlin" }).success).toBe(true); expect(Schema.safeParse({ employmentStatus: "in_training" }).success).toBe(true); expect(Schema.safeParse({ shiftWork: false }).success).toBe(true); }); it("accepts explicit nulls (= clear field)", () => { expect( Schema.safeParse({ city: null, birthYear: null, employmentStatus: null, shiftWork: null }).success, ).toBe(true); }); }); describe("demographics zod schema — invalid input must reject (not 500)", () => { it("rejects birthYear out of range (< 1920)", () => { const r = Schema.safeParse({ birthYear: 1800 }); expect(r.success).toBe(false); }); it("rejects birthYear out of range (> 2010 = under-13 protection)", () => { const r = Schema.safeParse({ birthYear: 2024 }); expect(r.success).toBe(false); }); it("rejects unknown gender enum", () => { const r = Schema.safeParse({ gender: "alien" }); expect(r.success).toBe(false); }); it("rejects unknown employmentStatus enum", () => { const r = Schema.safeParse({ employmentStatus: "freelancer" }); expect(r.success).toBe(false); }); it("rejects unknown jobTenure enum", () => { const r = Schema.safeParse({ jobTenure: "5_years" }); expect(r.success).toBe(false); }); it("rejects industry longer than 64 chars", () => { const r = Schema.safeParse({ industry: "x".repeat(65) }); expect(r.success).toBe(false); }); it("rejects bundesland with foreign-country code", () => { expect(Schema.safeParse({ bundesland: "AT-9" }).success).toBe(false); expect(Schema.safeParse({ bundesland: "BY" }).success).toBe(false); expect(Schema.safeParse({ bundesland: "DE-XX" }).success).toBe(false); }); it("accepts all 16 valid Bundesländer codes", () => { const codes = [ "DE-BW","DE-BY","DE-BE","DE-BB","DE-HB","DE-HH","DE-HE","DE-MV", "DE-NI","DE-NW","DE-RP","DE-SL","DE-SN","DE-ST","DE-SH","DE-TH", ]; for (const c of codes) { expect(Schema.safeParse({ bundesland: c }).success).toBe(true); } }); it("rejects profession longer than 80 chars (legacy field, still validated)", () => { const r = Schema.safeParse({ profession: "x".repeat(81) }); expect(r.success).toBe(false); }); it("rejects city longer than 80 chars", () => { const r = Schema.safeParse({ city: "x".repeat(81) }); expect(r.success).toBe(false); }); it("rejects shiftWork as non-boolean string", () => { const r = Schema.safeParse({ shiftWork: "yes" }); expect(r.success).toBe(false); }); });