Files
Wyndham-RSVN/client/src/tests/roomingListService.spec.ts
T

150 lines
5.2 KiB
TypeScript

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<string, string> = {}): 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<RoomingListGenerationError>)
})
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<RoomingListGenerationError>)
expect(unauthorizedHandler).toHaveBeenCalledOnce()
})
})