chore: 初始化TikTok短剧后端项目基础架构
创建了完整的NestJS项目结构,包含配置文件、核心模块、工具类、拦截器、过滤器等基础代码,实现了健康检查、用户认证、管理员RBAC、剧集管理、订单支付、媒体管理等核心功能的基础框架,配置了ESLint、TypeScript、Jest等开发工具链。
This commit is contained in:
237
test/core-modules.e2e-spec.ts
Normal file
237
test/core-modules.e2e-spec.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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('Core drama platform modules', () => {
|
||||
let app: INestApplication;
|
||||
let adminToken: string;
|
||||
let userToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrapApp(app);
|
||||
await app.init();
|
||||
adminToken = await loginAdmin(app);
|
||||
|
||||
const login = await request(app.getHttpServer())
|
||||
.post('/api/app/v1/auth/tiktok-login')
|
||||
.send({
|
||||
code: 'core-modules-code',
|
||||
mockOpenId: 'core-open-id',
|
||||
nickname: 'Core User',
|
||||
avatarUrl: 'https://cdn.example.com/avatar.png',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
userToken = login.body.data.accessToken;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('creates albums and episodes with TikTok, media, review, publish, and pricing fields', async () => {
|
||||
const cover = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/media/covers')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
fileName: 'cover.jpg',
|
||||
url: 'https://cdn.example.com/cover.jpg',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const uploadJob = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/media/upload-jobs')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
jobId: 'job-001',
|
||||
byteplusVid: 'v02-byteplus-001',
|
||||
status: 'completed',
|
||||
rawResponse: { provider: 'byteplus' },
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(uploadJob.body.data).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
jobId: 'job-001',
|
||||
byteplusVid: 'v02-byteplus-001',
|
||||
status: 'completed',
|
||||
rawResponse: { provider: 'byteplus' },
|
||||
});
|
||||
|
||||
const album = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
tiktokAlbumId: 'tk-album-001',
|
||||
title: 'Core Album',
|
||||
description: 'Album for core schema.',
|
||||
coverId: cover.body.data.id,
|
||||
coverUrl: cover.body.data.url,
|
||||
type: 'revenge',
|
||||
status: 'draft',
|
||||
onlineVersion: 1,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(album.body.data).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
tiktokAlbumId: 'tk-album-001',
|
||||
title: 'Core Album',
|
||||
coverId: cover.body.data.id,
|
||||
coverUrl: 'https://cdn.example.com/cover.jpg',
|
||||
status: 'draft',
|
||||
onlineVersion: 1,
|
||||
});
|
||||
|
||||
const episode = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
albumId: album.body.data.id,
|
||||
tiktokEpisodeId: 'tk-episode-001',
|
||||
seq: 1,
|
||||
title: 'Core Episode 1',
|
||||
byteplusVid: 'v02-byteplus-001',
|
||||
coverId: cover.body.data.id,
|
||||
coverUrl: cover.body.data.url,
|
||||
videoUrl: 'https://cdn.example.com/episode.mp4',
|
||||
freeType: 'paid',
|
||||
price: 299,
|
||||
reviewStatus: 'pending',
|
||||
publishStatus: 'draft',
|
||||
durationSeconds: 300,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
expect(episode.body.data).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
albumId: album.body.data.id,
|
||||
tiktokEpisodeId: 'tk-episode-001',
|
||||
seq: 1,
|
||||
byteplusVid: 'v02-byteplus-001',
|
||||
coverId: cover.body.data.id,
|
||||
freeType: 'paid',
|
||||
price: 299,
|
||||
reviewStatus: 'pending',
|
||||
publishStatus: 'draft',
|
||||
});
|
||||
});
|
||||
|
||||
it('supports review submission, publish status changes, player auth, trade orders, webhook unlocks, and unlock records', async () => {
|
||||
const album = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/dramas')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
title: 'Player Album',
|
||||
coverUrl: 'https://cdn.example.com/player-cover.jpg',
|
||||
type: 'player',
|
||||
status: 'published',
|
||||
onlineVersion: 1,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const episode = await request(app.getHttpServer())
|
||||
.post('/api/admin/v1/episodes')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({
|
||||
albumId: album.body.data.id,
|
||||
seq: 1,
|
||||
title: 'Paid Player Episode',
|
||||
byteplusVid: 'v02-player-001',
|
||||
coverUrl: 'https://cdn.example.com/player-episode.jpg',
|
||||
videoUrl: 'https://cdn.example.com/player-episode.mp4',
|
||||
freeType: 'paid',
|
||||
price: 399,
|
||||
reviewStatus: 'approved',
|
||||
publishStatus: 'published',
|
||||
durationSeconds: 300,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/api/admin/v1/dramas/${album.body.data.id}/submit-review`)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(201);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch(`/api/admin/v1/episodes/${episode.body.data.id}/publish-status`)
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.send({ publishStatus: 'published' })
|
||||
.expect(200);
|
||||
|
||||
const lockedPlayer = await request(app.getHttpServer())
|
||||
.get(`/api/app/v1/player/episodes/${episode.body.data.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(lockedPlayer.body.data).toMatchObject({
|
||||
albumId: album.body.data.id,
|
||||
episodeId: episode.body.data.id,
|
||||
vid: 'v02-player-001',
|
||||
hasPermission: false,
|
||||
playAuthToken: null,
|
||||
});
|
||||
|
||||
const order = await request(app.getHttpServer())
|
||||
.post('/api/app/v1/orders')
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.send({ episodeId: episode.body.data.id })
|
||||
.expect(201);
|
||||
|
||||
expect(order.body.data).toMatchObject({
|
||||
id: expect.any(Number),
|
||||
albumId: album.body.data.id,
|
||||
episodeId: episode.body.data.id,
|
||||
tradeOrderId: expect.stringMatching(/^TRADE/),
|
||||
amount: 399,
|
||||
status: 'pending',
|
||||
});
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/api/webhooks/tiktok/payments')
|
||||
.send({
|
||||
tradeOrderId: order.body.data.tradeOrderId,
|
||||
tiktokOrderId: 'tk-order-001',
|
||||
providerTransactionId: 'txn-core-001',
|
||||
status: 'paid',
|
||||
signature: 'dev-signature',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const unlockedPlayer = await request(app.getHttpServer())
|
||||
.get(`/api/app/v1/player/episodes/${episode.body.data.id}`)
|
||||
.set('Authorization', `Bearer ${userToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(unlockedPlayer.body.data).toMatchObject({
|
||||
albumId: album.body.data.id,
|
||||
episodeId: episode.body.data.id,
|
||||
vid: 'v02-player-001',
|
||||
hasPermission: true,
|
||||
playAuthToken: expect.stringMatching(/^play_auth_/),
|
||||
});
|
||||
|
||||
const unlocks = await request(app.getHttpServer())
|
||||
.get('/api/admin/v1/unlock-records')
|
||||
.set('Authorization', `Bearer ${adminToken}`)
|
||||
.expect(200);
|
||||
|
||||
expect(unlocks.body.data.list).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
userId: expect.any(Number),
|
||||
episodeId: episode.body.data.id,
|
||||
source: 'payment',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user