import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app/app.module'; import { bootstrapApp } from '../src/bootstrap'; import { loginAdmin } from './test-helpers'; describe('Data management and media upload flow', () => { let app: INestApplication; let adminToken: string; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleRef.createNestApplication(); bootstrapApp(app); await app.init(); adminToken = await loginAdmin(app); }); afterAll(async () => { await app.close(); }); it('creates a drama, configures episode slots, completes a direct upload, and exposes approved published content to app APIs', async () => { const drama = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Upload Flow Drama', description: 'Drama created by admin before uploading episodes.', coverUrl: 'https://cdn.example.com/upload-flow-cover.jpg', type: 'romance', region: 'TH', language: 'th', status: 'draft', }) .expect(201); const dramaId = drama.body.data.id as number; const slots = await request(app.getHttpServer()) .post(`/api/admin/v1/dramas/${dramaId}/episodes/batch`) .set('Authorization', `Bearer ${adminToken}`) .send({ episodeCount: 3 }) .expect(201); expect(slots.body.data.list).toHaveLength(3); expect(slots.body.data.list[0]).toMatchObject({ dramaId, episodeNo: 1, title: '第1集', publishStatus: 'draft', reviewStatus: 'not_submitted', }); const episodeId = slots.body.data.list[0].id as number; const uploadTask = await request(app.getHttpServer()) .post('/api/admin/v1/media/upload-tasks') .set('Authorization', `Bearer ${adminToken}`) .send({ episodeId, fileName: 'episode-1.mp4', contentType: 'video/mp4', fileSize: 1024, }) .expect(201); expect(uploadTask.body.data).toMatchObject({ jobId: expect.any(String), uploadUrl: expect.stringContaining('mock-byteplus-upload'), bucket: expect.any(String), objectKey: expect.stringContaining('episode-1.mp4'), status: 'pending', }); const completed = await request(app.getHttpServer()) .patch(`/api/admin/v1/media/upload-tasks/${uploadTask.body.data.jobId}/complete`) .set('Authorization', `Bearer ${adminToken}`) .send({}) .expect(200); expect(completed.body.data).toMatchObject({ jobId: uploadTask.body.data.jobId, episodeId, status: 'reviewing', byteplusVid: expect.stringMatching(/^mock_vid_/), width: 1080, height: 1920, durationSeconds: 180, }); await request(app.getHttpServer()) .post(`/api/admin/v1/media/upload-tasks/${uploadTask.body.data.jobId}/review/retry`) .set('Authorization', `Bearer ${adminToken}`) .expect(201); await request(app.getHttpServer()) .post('/api/admin/v1/media/review-status/sync') .set('Authorization', `Bearer ${adminToken}`) .expect(201); await request(app.getHttpServer()) .patch(`/api/admin/v1/episodes/${episodeId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ title: '正式第1集', coverUrl: 'https://cdn.example.com/episode-1-cover.jpg', freeType: 'free', publishStatus: 'published', }) .expect(200); await request(app.getHttpServer()) .patch(`/api/admin/v1/dramas/${dramaId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ status: 'published' }) .expect(200); const appDetail = await request(app.getHttpServer()) .get(`/api/app/v1/dramas/${dramaId}`) .expect(200); expect(appDetail.body.data).toMatchObject({ id: dramaId, title: 'Upload Flow Drama', totalEpisodes: 3, publishedEpisodes: 1, }); const appEpisodes = await request(app.getHttpServer()) .get(`/api/app/v1/dramas/${dramaId}/episodes`) .expect(200); expect(appEpisodes.body.data.list).toEqual([ expect.objectContaining({ id: episodeId, title: '正式第1集', reviewStatus: 'approved', publishStatus: 'published', byteplusVid: completed.body.data.byteplusVid, }), ]); }); it('syncs TikTok mock analytics and returns overview plus drama and episode metrics', async () => { const drama = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Analytics Drama', coverUrl: 'https://cdn.example.com/analytics-cover.jpg', type: 'action', status: 'published', }) .expect(201); const episode = await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send({ dramaId: drama.body.data.id, episodeNo: 1, title: 'Analytics Episode', coverUrl: 'https://cdn.example.com/analytics-episode.jpg', videoUrl: 'https://cdn.example.com/analytics-episode.mp4', byteplusVid: 'mock_vid_analytics', durationSeconds: 180, reviewStatus: 'approved', publishStatus: 'published', status: 'published', }) .expect(201); const sync = await request(app.getHttpServer()) .post('/api/admin/v1/data/sync') .set('Authorization', `Bearer ${adminToken}`) .send({ dramaId: drama.body.data.id }) .expect(201); expect(sync.body.data).toMatchObject({ status: 'completed', dramaCount: expect.any(Number), episodeCount: expect.any(Number), }); const overview = await request(app.getHttpServer()) .get('/api/admin/v1/data/overview') .set('Authorization', `Bearer ${adminToken}`) .expect(200); expect(overview.body.data).toMatchObject({ totalPlays: expect.any(Number), totalLikes: expect.any(Number), totalShares: expect.any(Number), totalComments: expect.any(Number), }); const dramaMetrics = await request(app.getHttpServer()) .get('/api/admin/v1/data/dramas') .set('Authorization', `Bearer ${adminToken}`) .expect(200); expect(dramaMetrics.body.data.list).toEqual( expect.arrayContaining([ expect.objectContaining({ dramaId: drama.body.data.id, title: 'Analytics Drama', plays: expect.any(Number), }), ]), ); const episodeMetrics = await request(app.getHttpServer()) .get(`/api/admin/v1/data/episodes?dramaId=${drama.body.data.id}`) .set('Authorization', `Bearer ${adminToken}`) .expect(200); expect(episodeMetrics.body.data.list).toEqual([ expect.objectContaining({ dramaId: drama.body.data.id, episodeId: episode.body.data.id, title: 'Analytics Episode', plays: expect.any(Number), }), ]); }); });