import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; import { bootstrapApp } from '../src/bootstrap'; import { loginAdmin } from './test-helpers'; describe('Dramas and episodes', () => { 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('allows admin APIs to create a drama and episodes with release metadata', async () => { const dramaResponse = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'City Revenge', description: 'A short drama series.', coverUrl: 'https://cdn.example.com/cover.jpg', portraitCoverUrl: 'https://cdn.example.com/portrait.jpg', landscapeCoverUrl: 'https://cdn.example.com/landscape.jpg', type: 'revenge', tags: ['urban', 'revenge'], region: 'US', language: 'en', year: 2026, status: 'published', sortOrder: 10, isRecommended: true, }) .expect(201); expect(dramaResponse.body.data).toMatchObject({ id: expect.any(Number), title: 'City Revenge', status: 'published', type: 'revenge', }); const dramaId = dramaResponse.body.data.id; const episodeResponse = await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send({ dramaId, episodeNo: 1, title: 'Episode 1', description: 'Opening episode.', coverUrl: 'https://cdn.example.com/ep1.jpg', videoUrl: 'https://cdn.example.com/ep1.mp4', trialDurationSeconds: 60, durationSeconds: 300, isPaid: true, unlockPrice: 199, publishAt: '2026-07-01T00:00:00.000Z', nextReleaseAt: '2026-07-02T00:00:00.000Z', status: 'published', }) .expect(201); expect(episodeResponse.body.data).toMatchObject({ id: expect.any(Number), dramaId, episodeNo: 1, nextReleaseAt: '2026-07-02T00:00:00.000Z', }); }); it('rejects duplicate episode numbers under the same drama', async () => { const drama = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Duplicate Episode Test', coverUrl: 'https://cdn.example.com/cover.jpg', type: 'romance', status: 'draft', }) .expect(201); const payload = { dramaId: drama.body.data.id, episodeNo: 1, title: 'Episode 1', coverUrl: 'https://cdn.example.com/ep1.jpg', videoUrl: 'https://cdn.example.com/ep1.mp4', durationSeconds: 300, status: 'draft', }; await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send(payload) .expect(201); const duplicate = await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send(payload) .expect(409); expect(duplicate.body).toMatchObject({ code: 409, data: {}, message: 'Episode number already exists in this drama', }); }); it('creates episode slots when a new drama declares total episodes', async () => { const drama = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Preconfigured Slots Drama', coverUrl: 'https://cdn.example.com/preconfigured-slots.jpg', type: 'romance', status: 'draft', totalEpisodes: 2, }) .expect(201); expect(drama.body.data).toMatchObject({ title: 'Preconfigured Slots Drama', totalEpisodes: 2, publishedEpisodes: 0, }); const episodes = await request(app.getHttpServer()) .get(`/api/admin/v1/dramas/${drama.body.data.id}/episodes`) .set('Authorization', `Bearer ${adminToken}`) .expect(200); expect(episodes.body.data.list).toEqual([ expect.objectContaining({ dramaId: drama.body.data.id, episodeNo: 1, title: '第1集', publishStatus: 'draft', }), expect.objectContaining({ dramaId: drama.body.data.id, episodeNo: 2, title: '第2集', publishStatus: 'draft', }), ]); }); it('updates an episode from the drama detail episodes endpoint', async () => { const drama = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Nested Episode Update Drama', coverUrl: 'https://cdn.example.com/nested-update.jpg', type: 'action', status: 'draft', totalEpisodes: 1, }) .expect(201); const episodes = await request(app.getHttpServer()) .get(`/api/admin/v1/dramas/${drama.body.data.id}/episodes`) .set('Authorization', `Bearer ${adminToken}`) .expect(200); const episodeId = episodes.body.data.list[0].id; const updated = await request(app.getHttpServer()) .patch(`/api/admin/v1/dramas/${drama.body.data.id}/episodes/${episodeId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Updated Nested Episode', description: 'Updated from drama detail.', durationSeconds: 240, freeType: 'paid', price: 199, }) .expect(200); expect(updated.body.data).toMatchObject({ id: episodeId, dramaId: drama.body.data.id, title: 'Updated Nested Episode', description: 'Updated from drama detail.', durationSeconds: 240, freeType: 'paid', isPaid: true, price: 199, }); }); it('returns only published dramas and published episodes to app APIs', async () => { const published = await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Published Drama', coverUrl: 'https://cdn.example.com/pub.jpg', type: 'action', status: 'published', }) .expect(201); await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Draft Drama', coverUrl: 'https://cdn.example.com/draft.jpg', type: 'action', status: 'draft', }) .expect(201); await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send({ dramaId: published.body.data.id, episodeNo: 1, title: 'Published Episode', coverUrl: 'https://cdn.example.com/ep1.jpg', videoUrl: 'https://cdn.example.com/ep1.mp4', durationSeconds: 300, status: 'published', }) .expect(201); await request(app.getHttpServer()) .post('/api/admin/v1/episodes') .set('Authorization', `Bearer ${adminToken}`) .send({ dramaId: published.body.data.id, episodeNo: 2, title: 'Draft Episode', coverUrl: 'https://cdn.example.com/ep2.jpg', videoUrl: 'https://cdn.example.com/ep2.mp4', durationSeconds: 300, status: 'draft', }) .expect(201); const dramas = await request(app.getHttpServer()) .get('/api/app/v1/dramas') .expect(200); expect(dramas.body.data.list).toEqual( expect.arrayContaining([ expect.objectContaining({ title: 'Published Drama' }), ]), ); expect(dramas.body.data.list).not.toEqual( expect.arrayContaining([expect.objectContaining({ title: 'Draft Drama' })]), ); const episodes = await request(app.getHttpServer()) .get(`/api/app/v1/dramas/${published.body.data.id}/episodes`) .expect(200); expect(episodes.body.data.list).toEqual([ expect.objectContaining({ episodeNo: 1, title: 'Published Episode', }), ]); }); it('filters published dramas by title and defaults app page size to 10', async () => { await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Searchable Moon Drama', coverUrl: 'https://cdn.example.com/searchable-moon.jpg', type: 'romance', status: 'published', }) .expect(201); await request(app.getHttpServer()) .post('/api/admin/v1/dramas') .set('Authorization', `Bearer ${adminToken}`) .send({ title: 'Hidden Sun Drama', coverUrl: 'https://cdn.example.com/hidden-sun.jpg', type: 'romance', status: 'published', }) .expect(201); const response = await request(app.getHttpServer()) .get('/api/app/v1/dramas') .query({ title: 'Moon' }) .expect(200); expect(response.body.data.pagination.pageSize).toBe(10); expect(response.body.data.list).toEqual( expect.arrayContaining([ expect.objectContaining({ title: 'Searchable Moon Drama' }), ]), ); expect(response.body.data.list).not.toEqual( expect.arrayContaining([ expect.objectContaining({ title: 'Hidden Sun Drama' }), ]), ); }); });