本次提交新增了完整的数据分析模块,包括数据同步、指标统计接口;实现了基于BytePlus的直传媒体上传流程,包含上传任务创建、完成、审核同步等功能;同时完善了剧集管理功能,新增了剧集配置、更新接口,扩展了审核状态枚举,补充了剧集与剧集指标的关联逻辑,还添加了播放器对外接口与相关测试用例。
232 lines
7.1 KiB
TypeScript
232 lines
7.1 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('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),
|
|
}),
|
|
]);
|
|
});
|
|
});
|