import { describe, it, expect, beforeEach, vi } from 'vitest'; import { DataSource, Repository } from 'typeorm'; import { OrderService } from '../../src/services/order.service'; import { OrderPersonAsset } from '../../src/entities/order-person-asset.entity'; import { EmploymentOrder } from '../../src/entities/employment-order.entity'; import { File } from '@d8d/core-module/file-module'; import { AssetType, AssetFileType } from '../../src/schemas/order.schema'; /** * OrderService 单元测试 * * 重点测试 getCompanyVideos 方法: * - 验证 leftJoin 能返回所有视频记录(包括没有关联订单的视频) * - 验证企业数据隔离正确性 */ describe('OrderService - getCompanyVideos', () => { let orderService: OrderService; let mockAssetRepository: Partial>; let mockQueryBuilder: any; beforeEach(() => { // 创建 mock queryBuilder mockQueryBuilder = { innerJoin: vi.fn().mockReturnThis(), leftJoin: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), andWhere: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), leftJoinAndSelect: vi.fn().mockReturnThis(), skip: vi.fn().mockReturnThis(), take: vi.fn().mockReturnThis(), getMany: vi.fn(), getCount: vi.fn() }; // 创建 mock repository mockAssetRepository = { createQueryBuilder: vi.fn().mockReturnValue(mockQueryBuilder) }; // 创建 OrderService 实例 orderService = new OrderService({ getRepository: vi.fn().mockReturnValue(mockAssetRepository) } as Partial as DataSource); }); describe('leftJoin vs innerJoin 行为验证', () => { it('应该使用 leftJoin 而不是 innerJoin 来获取企业视频', async () => { // 准备测试数据 const mockVideoAssets = [ { id: 1, orderId: 100, personId: 1, assetType: AssetType.WORK_VIDEO, assetFileType: AssetFileType.VIDEO, fileId: 1, relatedTime: new Date('2024-01-01'), createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), file: { id: 1, name: 'test-video.mp4', type: 'video/mp4', size: 1024000, path: 'videos/test-video.mp4', fullUrl: 'http://example.com/videos/test-video.mp4', uploadTime: new Date('2024-01-01') } } ]; mockQueryBuilder.getMany.mockResolvedValue(mockVideoAssets); mockQueryBuilder.getCount.mockResolvedValue(1); // 调用 getCompanyVideos 方法 const result = await orderService.getCompanyVideos(1, { assetType: AssetType.WORK_VIDEO, page: 1, pageSize: 10 }); // 验证结果 expect(result.data).toHaveLength(1); expect(result.total).toBe(1); // 关键验证:应该调用 leftJoin 而不是 innerJoin expect(mockQueryBuilder.leftJoin).toHaveBeenCalledWith( 'asset.order', 'order' ); expect(mockQueryBuilder.innerJoin).not.toHaveBeenCalled(); }); it('应该正确过滤企业数据(通过 companyId)', async () => { const mockVideoAssets = [ { id: 1, orderId: 100, personId: 1, assetType: AssetType.SALARY_VIDEO, assetFileType: AssetFileType.VIDEO, fileId: 1, relatedTime: new Date('2024-01-01'), createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), file: { id: 1, name: 'salary-video.mp4', type: 'video/mp4', size: 2048000, path: 'videos/salary-video.mp4', fullUrl: 'http://example.com/videos/salary-video.mp4', uploadTime: new Date('2024-01-01') } } ]; mockQueryBuilder.getMany.mockResolvedValue(mockVideoAssets); mockQueryBuilder.getCount.mockResolvedValue(1); // 调用方法,指定 companyId = 123 await orderService.getCompanyVideos(123, { page: 1, pageSize: 10 }); // 验证 WHERE 条件包含 companyId 过滤(修复后支持 order.companyId 为 NULL 的情况) expect(mockQueryBuilder.where).toHaveBeenCalledWith( '(order.companyId = :companyId OR order.companyId IS NULL)', { companyId: 123 } ); }); it('应该只返回视频类型的资产(assetFileType = video)', async () => { const mockVideoAssets = [ { id: 1, orderId: 100, personId: 1, assetType: AssetType.CHECKIN_VIDEO, assetFileType: AssetFileType.VIDEO, fileId: 1, relatedTime: new Date('2024-01-01'), createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), file: { id: 1, name: 'checkin-video.mp4', type: 'video/mp4', size: 512000, path: 'videos/checkin-video.mp4', fullUrl: 'http://example.com/videos/checkin-video.mp4', uploadTime: new Date('2024-01-01') } } ]; mockQueryBuilder.getMany.mockResolvedValue(mockVideoAssets); mockQueryBuilder.getCount.mockResolvedValue(1); await orderService.getCompanyVideos(1, { page: 1, pageSize: 10 }); // 验证 AND WHERE 条件包含视频文件类型过滤 expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( 'asset.assetFileType = :fileType', { fileType: 'video' } ); }); it('应该支持按资产类型过滤(assetType 参数)', async () => { const mockVideoAssets = []; mockQueryBuilder.getMany.mockResolvedValue(mockVideoAssets); mockQueryBuilder.getCount.mockResolvedValue(0); // 调用方法,指定 assetType 过滤 await orderService.getCompanyVideos(1, { assetType: AssetType.TAX_VIDEO, page: 1, pageSize: 10 }); // 验证调用了两次 andWhere:一次是视频文件类型,一次是资产类型 expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( 'asset.assetFileType = :fileType', { fileType: 'video' } ); expect(mockQueryBuilder.andWhere).toHaveBeenCalledWith( 'asset.assetType = :assetType', { assetType: AssetType.TAX_VIDEO } ); }); it('应该正确格式化返回数据', async () => { const mockVideoAssets = [ { id: 1, orderId: 100, personId: 1, assetType: AssetType.WORK_VIDEO, assetFileType: AssetFileType.VIDEO, fileId: 1, relatedTime: new Date('2024-01-01'), createTime: new Date('2024-01-01'), updateTime: new Date('2024-01-01'), file: { id: 1, name: 'test-video.mp4', type: 'video/mp4', size: 1024000, path: 'videos/test-video.mp4', fullUrl: 'http://example.com/videos/test-video.mp4', uploadTime: new Date('2024-01-01'), description: 'Test video description' } } ]; mockQueryBuilder.getMany.mockResolvedValue(mockVideoAssets); mockQueryBuilder.getCount.mockResolvedValue(1); const result = await orderService.getCompanyVideos(1, { page: 1, pageSize: 10 }); // 验证返回数据格式 expect(result.data[0]).toMatchObject({ id: 1, orderId: 100, personId: 1, assetType: AssetType.WORK_VIDEO, assetFileType: AssetFileType.VIDEO, fileId: 1, relatedTime: expect.any(Date), createTime: expect.any(Date), updateTime: expect.any(Date), file: { id: 1, name: 'test-video.mp4', type: 'video/mp4', size: 1024000, path: 'videos/test-video.mp4', fullUrl: 'http://example.com/videos/test-video.mp4', uploadTime: expect.any(Date), description: 'Test video description' } }); }); }); describe('分页和排序功能', () => { it('应该支持分页查询', async () => { mockQueryBuilder.getMany.mockResolvedValue([]); mockQueryBuilder.getCount.mockResolvedValue(0); await orderService.getCompanyVideos(1, { page: 2, pageSize: 20 }); // 验证分页参数正确应用 expect(mockQueryBuilder.skip).toHaveBeenCalledWith((2 - 1) * 20); expect(mockQueryBuilder.take).toHaveBeenCalledWith(20); }); it('应该支持按不同字段排序', async () => { mockQueryBuilder.getMany.mockResolvedValue([]); mockQueryBuilder.getCount.mockResolvedValue(0); // 测试按 createTime 排序 await orderService.getCompanyVideos(1, { sortBy: 'createTime', sortOrder: 'ASC', page: 1, pageSize: 10 }); expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith( 'asset.createTime', 'ASC' ); }); it('默认应该按 relatedTime 降序排序', async () => { mockQueryBuilder.getMany.mockResolvedValue([]); mockQueryBuilder.getCount.mockResolvedValue(0); await orderService.getCompanyVideos(1, { page: 1, pageSize: 10 }); expect(mockQueryBuilder.orderBy).toHaveBeenCalledWith( 'asset.relatedTime', 'DESC' ); }); }); });