import { beforeEach, describe, expect, it, vi } from 'vitest' import { clearStoredAccessToken, setStoredAccessToken } from '@/services/authSession' import { generateReservationRoomingListExcel, RoomingListGenerationError, } from '@/services/roomingListService' import { setUnauthorizedHandler } from '@/services/httpClient' import type { RoomingListGenerationInput } from '@/types/roomingList' function createInput(): RoomingListGenerationInput { return { file: new File(['passport names'], 'source.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }), hotelId: 'HOTEL-TEST', peoplePerRoom: 2, arrival: '2026-05-10', departure: '2026-05-16', roomType: 'DLX', paymentType: 'CA', nationality: 'CHN', } } function createExcelResponse(headers: Record = {}): Response { return new Response(new Blob(['xlsx-binary']), { status: 200, headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ...headers, }, }) } function createJsonResponse(payload: unknown, status: number): Response { return new Response(JSON.stringify(payload), { status, headers: { 'Content-Type': 'application/json', }, }) } describe('roomingListService', () => { beforeEach(() => { vi.restoreAllMocks() clearStoredAccessToken() setUnauthorizedHandler(null) }) it('uploads source Excel as FormData and returns the generated workbook blob', async () => { const input = createInput() const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue( createExcelResponse({ 'Content-Disposition': 'attachment; filename="rooming-list-HOTEL-TEST-20260718.xlsx"', }), ) setStoredAccessToken('session-token') const result = await generateReservationRoomingListExcel(input) expect(fetchMock).toHaveBeenCalledTimes(1) const [url, init] = fetchMock.mock.calls[0]! expect(url).toBe('/api/reservation/rooming-lists/generations') expect(init).toMatchObject({ method: 'POST', headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', Authorization: 'Bearer session-token', }, }) expect(init?.headers).not.toHaveProperty('Content-Type') expect(init?.body).toBeInstanceOf(FormData) const formData = init?.body as FormData expect(formData.get('file')).toBe(input.file) expect(formData.get('hotel_id')).toBe('HOTEL-TEST') expect(formData.get('people_per_room')).toBe('2') expect(formData.get('arrival')).toBe('2026-05-10') expect(formData.get('departure')).toBe('2026-05-16') expect(formData.get('room_type')).toBe('DLX') expect(formData.get('payment_type')).toBe('CA') expect(formData.get('nationality')).toBe('CHN') expect(formData.get('title')).toBeNull() expect(formData.get('rate_code')).toBeNull() expect(formData.get('adults')).toBeNull() expect(formData.get('children')).toBeNull() expect(formData.get('vip')).toBeNull() expect(formData.get('email')).toBeNull() expect(formData.get('id_type')).toBeNull() expect(formData.get('id_number')).toBeNull() expect(result.fileName).toBe('rooming-list-HOTEL-TEST-20260718.xlsx') expect(result.blob.type).toBe('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') }) it('uses the default file name when Content-Disposition does not provide one', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue(createExcelResponse()) const result = await generateReservationRoomingListExcel({ ...createInput(), hotelId: undefined, paymentType: '', nationality: '', }) expect(result.fileName).toBe('rooming-list.xlsx') }) it('throws a typed error with backend details for validation failures', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue( createJsonResponse( { error_code: 'ROOMING_LIST_VALIDATION_FAILED', message: 'Rooming List 字段校验失败。', details: ['people_per_room: 必填字段缺失。', 'departure: 必须晚于 arrival。'], }, 400, ), ) await expect(generateReservationRoomingListExcel(createInput())).rejects.toMatchObject({ status: 400, errorCode: 'ROOMING_LIST_VALIDATION_FAILED', message: 'Rooming List 字段校验失败。', details: ['people_per_room: 必填字段缺失。', 'departure: 必须晚于 arrival。'], } satisfies Partial) }) it('runs the shared unauthorized handler for auth session failures', async () => { const unauthorizedHandler = vi.fn() setUnauthorizedHandler(unauthorizedHandler) vi.spyOn(globalThis, 'fetch').mockResolvedValue( createJsonResponse( { error_code: 'AUTH_SESSION_INVALID', message: '登录已失效,请重新登录。', details: [], }, 401, ), ) await expect(generateReservationRoomingListExcel(createInput())).rejects.toMatchObject({ status: 401, errorCode: 'AUTH_SESSION_INVALID', } satisfies Partial) expect(unauthorizedHandler).toHaveBeenCalledOnce() }) })