import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' import { mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { defineComponent, h, ref } from 'vue' import { ZodError } from 'zod' import { apiClient } from '@/lib/axios' import { useTimetableMutations } from '@/composables/api/useTimetableMutations' vi.mock('@/lib/axios', () => { const post = vi.fn() const put = vi.fn() const get = vi.fn() const del = vi.fn() return { apiClient: { post, put, get, delete: del } } }) interface MockApi { post: ReturnType put: ReturnType get: ReturnType delete: ReturnType } const mocked = apiClient as unknown as MockApi function mountWithMutations() { const api: { value: ReturnType | null } = { value: null } const orgId = ref('org_1') const eventId = ref('ev_1') const dayId = ref('day_1') const Host = defineComponent({ setup() { api.value = useTimetableMutations({ orgId, eventId, dayId }) return () => h('div') }, }) const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }) mount(Host, { global: { plugins: [[VueQueryPlugin, { queryClient }]] } }) return { api } } describe('Zod parse failure on API responses', () => { beforeEach(() => vi.clearAllMocks()) it('move() throws a ZodError when the success payload omits required fields', async () => { // Backend renamed `cascaded` → `cascadedItems`, or removed `version`, etc. // Whatever the drift, our Zod schema must reject it loudly so GlitchTip // sees a contract violation instead of silently coercing into runtime // crashes deep in components that read `.lane_resolved`. mocked.post.mockResolvedValueOnce({ data: { success: true, data: { moved: { id: 'p1' /* missing nearly every required field */ }, cascaded: [], }, }, }) const { api } = mountWithMutations() await expect(api.value!.move.mutateAsync({ payload: { performance_id: 'p1', target_stage_id: 's1', target_start_at: '2026-07-10 19:00:00', target_end_at: '2026-07-10 20:00:00', target_lane: 0, version: 3, }, idempotencyKey: 'idem-test', })).rejects.toBeInstanceOf(ZodError) }) it('move() 409 with malformed errors payload also throws a ZodError', async () => { // The 409 path parses err.response.data.errors against // moveTimetableConflictSchema. A drift in the conflict shape (e.g. // backend renames `current_version` → `currentVersion`) must surface as // a ZodError, not as a "missing field" ReferenceError downstream. mocked.post.mockRejectedValueOnce({ response: { status: 409, data: { errors: { conflict: 'version_mismatch', // current_version + client_version + server_data are missing }, }, }, }) const { api } = mountWithMutations() await expect(api.value!.move.mutateAsync({ payload: { performance_id: 'p1', target_stage_id: 's1', target_start_at: '2026-07-10 19:00:00', target_end_at: '2026-07-10 20:00:00', target_lane: 0, version: 3, }, idempotencyKey: 'idem-test', })).rejects.toBeInstanceOf(ZodError) }) it('createStage() throws a ZodError when response is malformed', async () => { mocked.post.mockResolvedValueOnce({ data: { success: true, data: { id: 's2', name: 'X' /* missing color, sort_order, etc. */ }, }, }) const { api } = mountWithMutations() await expect( api.value!.createStage.mutateAsync({ name: 'X', color: '#aabbcc' }), ).rejects.toBeInstanceOf(ZodError) }) })