|
|
@@ -0,0 +1,303 @@
|
|
|
+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<Repository<OrderPersonAsset>>;
|
|
|
+ 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<DataSource> 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'
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|