import { AdminService } from '../src/admin/admin.service'; import { DataService } from '../src/data/data.service'; import { TikTokDataProvider } from '../src/data/data.provider'; import { PublishStatus } from '../src/dramas/dramas.types'; import { DramasService } from '../src/dramas/dramas.service'; import { LogsService } from '../src/logs/logs.service'; import { UsersService } from '../src/users/users.service'; function createRepository() { const records: T[] = []; const matchesWhere = (item: T, where?: Partial) => { if (!where) { return true; } return Object.entries(where).every( ([key, value]) => item[key as keyof T] === value, ); }; return { records, create: jest.fn((input: Partial) => input as T), save: jest.fn(async (input: T) => { if (!input.id) { input.id = records.length + 1; } const index = records.findIndex((item) => item.id === input.id); if (index >= 0) { records[index] = input; } else { records.push(input); } return input; }), findOne: jest.fn(async ({ where }: { where: Partial }) => records.find((item) => matchesWhere(item, where)) ?? null, ), find: jest.fn(async () => [...records]), findAndCount: jest.fn( async ({ where, skip = 0, take = records.length }: { where?: Partial; skip?: number; take?: number }) => { const filtered = records.filter((item) => matchesWhere(item, where)); return [filtered.slice(skip, skip + take), filtered.length]; }, ), count: jest.fn(async ({ where }: { where?: Partial } = {}) => records.filter((item) => matchesWhere(item, where)).length, ), }; } function createDataSource(repositories: Record) { return { isInitialized: true, getRepository: jest.fn((entity: { name: string }) => { const repository = repositories[entity.name]; if (!repository) { throw new Error(`Missing repository for ${entity.name}`); } return repository; }), }; } describe('database-backed persistence', () => { it('persists permissions and admin profile changes through TypeORM repositories', async () => { const permissionRepository = createRepository(); const roleRepository = createRepository(); const adminUserRepository = createRepository(); const dataSource = createDataSource({ Permission: permissionRepository, Role: roleRepository, AdminUser: adminUserRepository, }); const service = new AdminService( { signAsync: jest.fn() } as never, dataSource as never, ); await service.onModuleInit(); const permissions = await service.listPermissions({ page: 1, pageSize: 50 }); expect(permissions.list).toEqual( expect.arrayContaining([ expect.objectContaining({ code: 'permission:read', name: '查看权限' }), expect.objectContaining({ code: 'data:read', name: '查看数据' }), ]), ); const updated = await service.updateProfile(1, { nickname: '数据库管理员', email: 'db-admin@example.com', }); expect(updated).toMatchObject({ id: 1, nickname: '数据库管理员', email: 'db-admin@example.com', }); expect(adminUserRepository.save).toHaveBeenCalledWith( expect.objectContaining({ nickname: '数据库管理员', email: 'db-admin@example.com', }), ); }); it('persists app users through TypeORM repositories when a DataSource is configured', async () => { const userRepository = createRepository(); const dataSource = createDataSource({ User: userRepository }); const service = new UsersService(dataSource as never); const result = await service.upsertTikTokUser({ tiktokOpenId: 'open-db-1', nickname: 'DB User', }); expect(result.isNewUser).toBe(true); expect(userRepository.save).toHaveBeenCalledWith( expect.objectContaining({ tiktokOpenId: 'open-db-1', nickname: 'DB User', status: 'active', }), ); }); it('persists request logs and playback error logs through TypeORM repositories', async () => { const requestLogRepository = createRepository(); const playbackErrorLogRepository = createRepository(); const dataSource = createDataSource({ RequestLog: requestLogRepository, PlaybackErrorLog: playbackErrorLogRepository, }); const service = new LogsService(dataSource as never); await service.recordRequest({ requestId: '00000000-0000-4000-8000-000000000001', method: 'GET', path: '/api/admin/v1/logs', statusCode: 200, responseCode: 0, message: 'success', costMs: 3, }); await service.createPlaybackError({ requestId: '00000000-0000-4000-8000-000000000002', dramaId: 1, episodeId: 2, playUrl: 'https://cdn.example.com/video.mp4', playerErrorCode: 'NETWORK', message: 'network error', occurredAt: '2026-07-01T00:00:00.000Z', }); expect(requestLogRepository.save).toHaveBeenCalled(); expect(playbackErrorLogRepository.save).toHaveBeenCalled(); }); it('persists synced drama and episode metrics through TypeORM repositories', async () => { const dramaMetricRepository = createRepository(); const episodeMetricRepository = createRepository(); const syncJobRepository = createRepository(); const dataSource = createDataSource({ DramaMetric: dramaMetricRepository, EpisodeMetric: episodeMetricRepository, DataSyncJob: syncJobRepository, }); const dramasService = new DramasService(); const tiktokDataProvider = new TikTokDataProvider(); const service = new DataService( dramasService, tiktokDataProvider, dataSource as never, ); const drama = await dramasService.createDrama({ title: 'DB Metrics', coverUrl: 'https://cdn.example.com/cover.jpg', type: 'action', status: PublishStatus.Published, }); await dramasService.createEpisode({ dramaId: drama.id, episodeNo: 1, title: 'DB Metrics Episode', coverUrl: drama.coverUrl, videoUrl: 'https://cdn.example.com/video.mp4', durationSeconds: 180, status: PublishStatus.Published, }); await service.sync({ dramaId: drama.id }); expect(dramaMetricRepository.save).toHaveBeenCalled(); expect(episodeMetricRepository.save).toHaveBeenCalled(); expect(syncJobRepository.save).toHaveBeenCalled(); }); it('aggregates episode metric plays for database-backed drama lists', async () => { const now = new Date('2026-07-02T00:00:00.000Z'); const dramaRepository = createRepository(); const episodeRepository = createRepository(); const queryBuilder = { select: jest.fn().mockReturnThis(), addSelect: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), groupBy: jest.fn().mockReturnThis(), getRawMany: jest.fn(async () => [{ dramaId: '1', plays: '321' }]), }; const episodeMetricRepository = { createQueryBuilder: jest.fn(() => queryBuilder), }; const dataSource = createDataSource({ Drama: dramaRepository, Episode: episodeRepository, EpisodeMetric: episodeMetricRepository, }); const service = new DramasService(dataSource as never); await dramaRepository.save({ id: 1, title: 'DB Plays Drama', coverUrl: 'https://cdn.example.com/db-plays.jpg', type: 'action', tags: [], status: PublishStatus.Published, sortOrder: 0, isRecommended: false, onlineVersion: 1, createdAt: now, updatedAt: now, }); await episodeRepository.save({ id: 1, dramaId: 1, episodeNo: 1, title: 'Episode 1', coverUrl: 'https://cdn.example.com/ep1.jpg', durationSeconds: 180, isPaid: false, freeType: 'free', price: 0, status: PublishStatus.Published, publishStatus: PublishStatus.Published, reviewStatus: 'approved', createdAt: now, updatedAt: now, }); const adminList = await service.listAdminDramas({ page: 1, pageSize: 10 }); const appList = await service.listPublishedDramas({ page: 1, pageSize: 10 }); expect(adminList.list[0]).toMatchObject({ title: 'DB Plays Drama', plays: 321, }); expect(appList.list[0]).toMatchObject({ title: 'DB Plays Drama', plays: 321, }); expect(queryBuilder.where).toHaveBeenCalledWith( 'metric.dramaId IN (:...dramaIds)', { dramaIds: [1] }, ); }); });