import { DataSource, Repository } from 'typeorm'; import { Bed } from '../entities/bed.entity'; import { Locker } from '../entities/locker.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { Room } from '../entities/room.entity'; import { RoomExpense } from '../entities/room-expense.entity'; import { RoomsService } from './rooms.service'; import { RoomsController } from './rooms.controller'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; // ── Controller test mocks ────────────────────────────────────────────── const mockExcelEachRow = jest.fn(); jest.mock('exceljs', () => ({ Workbook: jest.fn().mockImplementation(() => ({ xlsx: { load: jest.fn().mockResolvedValue(undefined) }, worksheets: [{ eachRow: mockExcelEachRow }], })), })); jest.mock('../common/request-utils', () => ({ extractRequestInfo: jest.fn(() => ({ ipAddress: '::1', userAgent: 'jest' })), })); // ── Service: parseRoomNumber boundary conditions ─────────────────────── describe('RoomsService — parseRoomNumber boundary conditions', () => { // 1 it('returns default {capacity:4, roomType:"四人间"} for unrecognised formats', () => { expect(RoomsService.parseRoomNumber('A101')).toEqual({ capacity: 4, roomType: '四人间' }); expect(RoomsService.parseRoomNumber('仓库')).toEqual({ capacity: 4, roomType: '四人间' }); expect(RoomsService.parseRoomNumber('')).toEqual({ capacity: 4, roomType: '四人间' }); }); // 2 it('preserves floor 0 — parseInt("0") should not become undefined via ||', () => { // "4-0" in standard format: roomPart = "0", charAt(0) = "0", // parseInt("0", 10) = 0, which is falsy but NOT NaN. // The code must use Number.isNaN(), not ||, to keep floor: 0. const result = RoomsService.parseRoomNumber('4-0'); expect(result.floor).toBe(0); expect(result.floor).not.toBeUndefined(); }); // 3 — moved below into the batchImport describe (needs instantiated service) // 4 it('parses standard format "4-102" correctly', () => { const result = RoomsService.parseRoomNumber('4-102'); expect(result).toEqual({ building: '4号楼', floor: 1, roomType: '四人间', capacity: 4, }); }); // 5 it('recognises building 2 as 单人间 (capacity 1)', () => { const result = RoomsService.parseRoomNumber('2-301'); expect(result).toEqual({ building: '2号楼', floor: 3, roomType: '单人间', capacity: 1, }); }); // 6 it('recognises building 8 as 爆改房 (capacity 2)', () => { const result = RoomsService.parseRoomNumber('8-102'); expect(result).toEqual({ building: '8号楼', floor: 1, roomType: '爆改房', capacity: 2, }); }); // 7 it('parses family format "1-2-101" correctly', () => { const result = RoomsService.parseRoomNumber('1-2-101'); expect(result).toEqual({ building: '1-2栋', floor: 1, roomType: '家庭房', capacity: 4, }); }); }); // ── Service: batchImport boundary conditions ─────────────────────────── describe('RoomsService — batchImport boundary conditions', () => { const createService = () => { const saved: any[] = []; const roomRepo = { findOne: jest.fn().mockResolvedValue(null), create: jest.fn((data: any) => data), save: jest.fn().mockImplementation((entity: any) => { const record = { id: saved.length + 1, ...entity }; saved.push(record); return Promise.resolve(record); }), } as unknown as Repository; const bedRepo = { create: jest.fn((data: any) => data), save: jest.fn().mockResolvedValue([]), } as unknown as Repository; const occupancyRepo = {} as unknown as Repository; const roomExpRepo = {} as unknown as Repository; const lockerRepo = {} as unknown as Repository; const dataSource = {} as unknown as DataSource; const service = new RoomsService( roomRepo, occupancyRepo, roomExpRepo, bedRepo, lockerRepo, dataSource, ); return { service, roomRepo, bedRepo }; }; // 3 it('preserves explicit capacity 0 via ?? (not ||)', async () => { const { service, roomRepo } = createService(); await service.batchImport([ { roomNumber: '4-102', capacity: 0 }, ]); // If || were used, 0 would be falsy and fall through to parsed.capacity (4). // Because ?? is used, explicit 0 stays 0. expect(roomRepo.save).toHaveBeenCalledWith( expect.objectContaining({ capacity: 0, roomNumber: '4-102' }), ); expect(roomRepo.save).toHaveBeenCalledTimes(1); }); it('falls back to parsed capacity when row.capacity is undefined', async () => { const { service, roomRepo } = createService(); await service.batchImport([ { roomNumber: '4-102' }, // no capacity field { roomNumber: '2-301' }, // building 2 → parsed capacity 1 ]); const calls = (roomRepo.save as jest.Mock).mock.calls; expect(calls[0][0]).toMatchObject({ capacity: 4, roomNumber: '4-102' }); expect(calls[1][0]).toMatchObject({ capacity: 1, roomNumber: '2-301' }); }); it('falls back to the default 4 when both row.capacity and parsed.capacity are undefined', async () => { const { service, roomRepo } = createService(); // "A101" is unrecognised → parsed = { capacity: 4, roomType: '四人间' } // row has no capacity → row.capacity ?? parsed.capacity ?? 4 = 4 await service.batchImport([ { roomNumber: 'A101' }, ]); expect(roomRepo.save).toHaveBeenCalledWith( expect.objectContaining({ capacity: 4, roomNumber: 'A101' }), ); }); }); // ── Controller: boundary conditions ──────────────────────────────────── describe('RoomsController — boundary conditions', () => { const mockLogService = { log: jest.fn().mockResolvedValue(undefined), } as unknown as OperationLogsService; const mockRoomsService = { batchImport: jest.fn().mockResolvedValue({ message: '成功导入 1 间宿舍,跳过 0 条(重复或空行)', imported: 1, skipped: 0, }), } as unknown as RoomsService; beforeEach(() => { jest.clearAllMocks(); }); it('importExcel passes capacity=4 when Excel cell is 0 (|| instead of ??)', async () => { mockExcelEachRow.mockImplementation((cb: (row: any, idx: number) => void) => { // idx 1 = header → skip // idx 2 = data row const dataRow = { getCell: jest.fn((col: number) => { const values: Record = { 1: '4-102', 2: '', 3: 1, 4: 0, // <-- capacity explicitly 0 in spreadsheet 5: '', 6: '', 7: '', }; return { value: values[col] ?? '' }; }), }; cb(dataRow, 2); }); const controller = new RoomsController(mockRoomsService, mockLogService); const file = { buffer: Buffer.from('fake') } as Express.Multer.File; const req = { user: { id: 1, username: 'tester' } }; await controller.importExcel(file, req); expect(mockRoomsService.batchImport).toHaveBeenCalledTimes(1); const rows = (mockRoomsService.batchImport as jest.Mock).mock.calls[0][0]; expect(rows).toHaveLength(1); // Bug: || turns 0 into 4 — capacity should be 4, not 0 expect(rows[0].capacity).toBe(4); }); });