Files
cyh-tk-backend/test/dramas.e2e-spec.ts
zhxiao1124 ef8c31d3be chore: 初始化TikTok短剧后端项目基础架构
创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
2026-07-01 13:55:37 +08:00

201 lines
5.8 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('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',
}),
]);
});
});