feat(timetable): validate API responses against Zod schemas at runtime

Per Phase A finding A5 — Zod schemas in @/schemas/timetable.ts were
types-only; nothing parsed actual server responses. Backend → frontend
contract drift would only surface as TypeError deep in components.

useTimetable.ts queries now parse:
  - useStages       → stageArraySchema.parse()
  - usePerformances → performanceArraySchema.parse()
  - useWachtrij     → performanceArraySchema.parse()
  - useEngagement   → artistEngagementSchema.parse()

useTimetableMutations.ts mutations now parse:
  - move success    → moveTimetableSuccessSchema.parse()
  - move 409 errors → moveTimetableConflictSchema.parse() (the .errors
                      sub-object — see backend canon at TimetableMoveController:64)
  - create / updateNotes → performanceSchema.parse()
  - createStage / updateStage → stageSchema.parse()

The move() success parse runs OUTSIDE the try/catch so a Zod failure on
a 200 response surfaces as a true error rather than being misclassified
as a 409. Per Phase A finding A8 the conflict shape already matches
backend field-for-field; no schema correction needed, but the parse()
locks future drift in.

Regression test (tests/unit/composables/api/zodParseFailure.test.ts):
  - move() success with missing fields → rejects with ZodError
  - move() 409 with malformed errors payload → rejects with ZodError
  - createStage() with missing fields → rejects with ZodError

Existing test fixture for createStage was missing created_at/updated_at;
fixed in same commit (real backend responses always include them).

Test count: 321 → 324.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 03:32:21 +02:00
parent 5f135ec2b9
commit 8d1cb39172
4 changed files with 176 additions and 13 deletions

View File

@@ -188,7 +188,19 @@ describe('useTimetableMutations', () => {
describe('createStage', () => {
it('hits POST /stages', async () => {
mocked.post.mockResolvedValueOnce({
data: { success: true, data: { id: 's2', name: 'New Stage', color: '#aabbcc', capacity: 1000, sort_order: 1, event_id: 'ev_1' } },
data: {
success: true,
data: {
id: 's2',
name: 'New Stage',
color: '#aabbcc',
capacity: 1000,
sort_order: 1,
event_id: 'ev_1',
created_at: '2026-07-10T18:00:00.000Z',
updated_at: '2026-07-10T18:00:00.000Z',
},
},
})
const { api } = mountWithMutations()