|
@@ -0,0 +1,264 @@
|
|
|
|
|
+import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
+import { DataSource, Repository, ObjectLiteral } from 'typeorm';
|
|
|
|
|
+import { GenericCrudService, UserTrackingOptions, RelationFieldOptions } from '../../src/services/generic-crud.service';
|
|
|
|
|
+
|
|
|
|
|
+// 测试实体类
|
|
|
|
|
+class TestEntity implements ObjectLiteral {
|
|
|
|
|
+ id!: number;
|
|
|
|
|
+ name!: string;
|
|
|
|
|
+ createdBy?: string;
|
|
|
|
|
+ updatedBy?: string;
|
|
|
|
|
+ userId?: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 测试关联实体类
|
|
|
|
|
+class RelatedEntity implements ObjectLiteral {
|
|
|
|
|
+ id!: number;
|
|
|
|
|
+ name!: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+describe('GenericCrudService', () => {
|
|
|
|
|
+ let mockDataSource: DataSource;
|
|
|
|
|
+ let mockRepository: Repository<TestEntity>;
|
|
|
|
|
+ let crudService: GenericCrudService<TestEntity>;
|
|
|
|
|
+
|
|
|
|
|
+ beforeEach(() => {
|
|
|
|
|
+ // 创建模拟数据源和仓库
|
|
|
|
|
+ mockRepository = {
|
|
|
|
|
+ createQueryBuilder: vi.fn().mockReturnValue({
|
|
|
|
|
+ leftJoinAndSelect: vi.fn().mockReturnThis(),
|
|
|
|
|
+ andWhere: vi.fn().mockReturnThis(),
|
|
|
|
|
+ orderBy: vi.fn().mockReturnThis(),
|
|
|
|
|
+ skip: vi.fn().mockReturnThis(),
|
|
|
|
|
+ take: vi.fn().mockReturnThis(),
|
|
|
|
|
+ getManyAndCount: vi.fn().mockResolvedValue([[], 0])
|
|
|
|
|
+ }),
|
|
|
|
|
+ findOne: vi.fn().mockResolvedValue(null),
|
|
|
|
|
+ create: vi.fn().mockImplementation((data) => ({ ...data, id: 1 })),
|
|
|
|
|
+ save: vi.fn().mockImplementation((entity) => Promise.resolve(entity)),
|
|
|
|
|
+ update: vi.fn().mockResolvedValue({ affected: 1 }),
|
|
|
|
|
+ delete: vi.fn().mockResolvedValue({ affected: 1 })
|
|
|
|
|
+ } as any;
|
|
|
|
|
+
|
|
|
|
|
+ mockDataSource = {
|
|
|
|
|
+ getRepository: vi.fn().mockReturnValue(mockRepository)
|
|
|
|
|
+ } as any;
|
|
|
|
|
+
|
|
|
|
|
+ crudService = new GenericCrudService(mockDataSource, TestEntity);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('构造函数', () => {
|
|
|
|
|
+ it('应该正确初始化仓库', () => {
|
|
|
|
|
+ expect(mockDataSource.getRepository).toHaveBeenCalledWith(TestEntity);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持用户跟踪选项', () => {
|
|
|
|
|
+ const userTrackingOptions: UserTrackingOptions = {
|
|
|
|
|
+ createdByField: 'creator',
|
|
|
|
|
+ updatedByField: 'updater',
|
|
|
|
|
+ userIdField: 'ownerId'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const serviceWithTracking = new GenericCrudService(
|
|
|
|
|
+ mockDataSource,
|
|
|
|
|
+ TestEntity,
|
|
|
|
|
+ { userTracking: userTrackingOptions }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ expect(serviceWithTracking).toBeDefined();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持关联字段选项', () => {
|
|
|
|
|
+ const relationFields: RelationFieldOptions = {
|
|
|
|
|
+ relatedIds: {
|
|
|
|
|
+ relationName: 'relatedEntities',
|
|
|
|
|
+ targetEntity: RelatedEntity
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const serviceWithRelations = new GenericCrudService(
|
|
|
|
|
+ mockDataSource,
|
|
|
|
|
+ TestEntity,
|
|
|
|
|
+ { relationFields }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ expect(serviceWithRelations).toBeDefined();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('getList 方法', () => {
|
|
|
|
|
+ it('应该调用查询构建器获取列表', async () => {
|
|
|
|
|
+ const mockQueryBuilder = {
|
|
|
|
|
+ leftJoinAndSelect: vi.fn().mockReturnThis(),
|
|
|
|
|
+ andWhere: vi.fn().mockReturnThis(),
|
|
|
|
|
+ orderBy: vi.fn().mockReturnThis(),
|
|
|
|
|
+ skip: vi.fn().mockReturnThis(),
|
|
|
|
|
+ take: vi.fn().mockReturnThis(),
|
|
|
|
|
+ getManyAndCount: vi.fn().mockResolvedValue([[], 0])
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder as any);
|
|
|
|
|
+
|
|
|
|
|
+ const [data, total] = await crudService.getList(1, 10);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('entity');
|
|
|
|
|
+ expect(mockQueryBuilder.skip).toHaveBeenCalledWith(0);
|
|
|
|
|
+ expect(mockQueryBuilder.take).toHaveBeenCalledWith(10);
|
|
|
|
|
+ expect(data).toEqual([]);
|
|
|
|
|
+ expect(total).toBe(0);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持关键词搜索', async () => {
|
|
|
|
|
+ const mockQueryBuilder = {
|
|
|
|
|
+ leftJoinAndSelect: vi.fn().mockReturnThis(),
|
|
|
|
|
+ andWhere: vi.fn().mockReturnThis(),
|
|
|
|
|
+ orderBy: vi.fn().mockReturnThis(),
|
|
|
|
|
+ skip: vi.fn().mockReturnThis(),
|
|
|
|
|
+ take: vi.fn().mockReturnThis(),
|
|
|
|
|
+ getManyAndCount: vi.fn().mockResolvedValue([[], 0])
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder as any);
|
|
|
|
|
+
|
|
|
|
|
+ await crudService.getList(1, 10, 'test', ['name']);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockQueryBuilder.andWhere).toHaveBeenCalled();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('getById 方法', () => {
|
|
|
|
|
+ it('应该根据ID获取实体', async () => {
|
|
|
|
|
+ const testEntity = { id: 1, name: 'Test' };
|
|
|
|
|
+ vi.mocked(mockRepository.findOne).mockResolvedValue(testEntity as any);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.getById(1);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.findOne).toHaveBeenCalledWith({
|
|
|
|
|
+ where: { id: 1 },
|
|
|
|
|
+ relations: []
|
|
|
|
|
+ });
|
|
|
|
|
+ expect(result).toEqual(testEntity);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持关联关系', async () => {
|
|
|
|
|
+ await crudService.getById(1, ['relatedEntity']);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.findOne).toHaveBeenCalledWith({
|
|
|
|
|
+ where: { id: 1 },
|
|
|
|
|
+ relations: ['relatedEntity']
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('create 方法', () => {
|
|
|
|
|
+ it('应该创建新实体', async () => {
|
|
|
|
|
+ const createData = { name: 'New Entity' };
|
|
|
|
|
+ const savedEntity = { id: 1, ...createData };
|
|
|
|
|
+
|
|
|
|
|
+ vi.mocked(mockRepository.create).mockReturnValue(savedEntity as any);
|
|
|
|
|
+ vi.mocked(mockRepository.save).mockResolvedValue(savedEntity as any);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.create(createData);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.create).toHaveBeenCalledWith(createData);
|
|
|
|
|
+ expect(mockRepository.save).toHaveBeenCalledWith(savedEntity);
|
|
|
|
|
+ expect(result).toEqual(savedEntity);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该支持用户跟踪', async () => {
|
|
|
|
|
+ const userTrackingOptions: UserTrackingOptions = {
|
|
|
|
|
+ createdByField: 'createdBy',
|
|
|
|
|
+ updatedByField: 'updatedBy'
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const serviceWithTracking = new GenericCrudService(
|
|
|
|
|
+ mockDataSource,
|
|
|
|
|
+ TestEntity,
|
|
|
|
|
+ { userTracking: userTrackingOptions }
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const createData = { name: 'New Entity' };
|
|
|
|
|
+ const savedEntity = { id: 1, ...createData, createdBy: 'user123', updatedBy: 'user123' };
|
|
|
|
|
+
|
|
|
|
|
+ vi.mocked(mockRepository.create).mockReturnValue(savedEntity as any);
|
|
|
|
|
+ vi.mocked(mockRepository.save).mockResolvedValue(savedEntity as any);
|
|
|
|
|
+
|
|
|
|
|
+ await serviceWithTracking.create(createData, 'user123');
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.create).toHaveBeenCalledWith({
|
|
|
|
|
+ ...createData,
|
|
|
|
|
+ createdBy: 'user123',
|
|
|
|
|
+ updatedBy: 'user123',
|
|
|
|
|
+ userId: 'user123'
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('update 方法', () => {
|
|
|
|
|
+ it('应该更新现有实体', async () => {
|
|
|
|
|
+ const updateData = { name: 'Updated Entity' };
|
|
|
|
|
+ const existingEntity = { id: 1, name: 'Original Entity' };
|
|
|
|
|
+ const updatedEntity = { ...existingEntity, ...updateData };
|
|
|
|
|
+
|
|
|
|
|
+ vi.mocked(mockRepository.findOne).mockResolvedValue(existingEntity as any);
|
|
|
|
|
+ vi.mocked(mockRepository.save).mockResolvedValue(updatedEntity as any);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.update(1, updateData);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.update).toHaveBeenCalledWith(1, updateData);
|
|
|
|
|
+ expect(mockRepository.findOne).toHaveBeenCalledWith({
|
|
|
|
|
+ where: { id: 1 },
|
|
|
|
|
+ relations: []
|
|
|
|
|
+ });
|
|
|
|
|
+ expect(result).toEqual(updatedEntity);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该返回 null 当实体不存在时', async () => {
|
|
|
|
|
+ vi.mocked(mockRepository.findOne).mockResolvedValue(null);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.update(999, { name: 'Updated' });
|
|
|
|
|
+
|
|
|
|
|
+ expect(result).toBeNull();
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('delete 方法', () => {
|
|
|
|
|
+ it('应该删除实体', async () => {
|
|
|
|
|
+ vi.mocked(mockRepository.delete).mockResolvedValue({ affected: 1 } as any);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.delete(1);
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.delete).toHaveBeenCalledWith(1);
|
|
|
|
|
+ expect(result).toBe(true);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该返回 false 当实体不存在时', async () => {
|
|
|
|
|
+ vi.mocked(mockRepository.delete).mockResolvedValue({ affected: 0 } as any);
|
|
|
|
|
+
|
|
|
|
|
+ const result = await crudService.delete(999);
|
|
|
|
|
+
|
|
|
|
|
+ expect(result).toBe(false);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ describe('createQueryBuilder 方法', () => {
|
|
|
|
|
+ it('应该返回查询构建器', () => {
|
|
|
|
|
+ const mockQueryBuilder = {} as any;
|
|
|
|
|
+ vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder);
|
|
|
|
|
+
|
|
|
|
|
+ const result = crudService.createQueryBuilder('test');
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('test');
|
|
|
|
|
+ expect(result).toBe(mockQueryBuilder);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ it('应该使用默认别名', () => {
|
|
|
|
|
+ const mockQueryBuilder = {} as any;
|
|
|
|
|
+ vi.mocked(mockRepository.createQueryBuilder).mockReturnValue(mockQueryBuilder);
|
|
|
|
|
+
|
|
|
|
|
+ crudService.createQueryBuilder();
|
|
|
|
|
+
|
|
|
|
|
+ expect(mockRepository.createQueryBuilder).toHaveBeenCalledWith('entity');
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+});
|