Files
cyh-tk-backend/test/dramas.e2e-spec.ts
zhxiao1124 0eeeea0c4d feat: 新增剧集封面、集数管理与上传优化功能
1. 新增Cover实体类并注册到数据库模块
2. 为剧集添加总集数字段及自动创建集数槽功能
3. 新增管理员更新剧集集数接口
4. 优化发布剧集列表:支持标题过滤与默认分页大小10
5. 扩展MediaUploadJob实体字段,完善媒体上传流程
6. 重构订单与媒体服务,新增数据库持久化支持
7. 新增剧集标题过滤的端到端测试用例
2026-07-02 15:46:29 +08:00

325 lines
9.6 KiB
TypeScript

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' }),
]),
);
});
});