1. 重构项目文件结构,统一文件存放位置与命名规则 2. 配置tsconfig路径别名,替换原有相对路径导入 3. 迁移drama-status枚举到dramas.types统一管理 4. 新增全局配置文件与工具类,补充完整注释与文档 5. 修复所有导入路径与依赖引用问题
292 lines
8.6 KiB
TypeScript
292 lines
8.6 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
import * as request from 'supertest';
|
|
import { AppModule } from '../src/app/app.module';
|
|
import { bootstrapApp } from '../src/bootstrap';
|
|
import { loginAdmin } from './test-helpers';
|
|
|
|
describe('Auth, orders, and playback', () => {
|
|
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('logs in with TikTok identity, creates an order, and unlocks paid playback through an idempotent payment webhook', async () => {
|
|
const login = await request(app.getHttpServer())
|
|
.post('/api/app/v1/auth/tiktok-login')
|
|
.send({
|
|
code: 'test-code',
|
|
mockOpenId: 'open-user-1',
|
|
nickname: 'Tester',
|
|
})
|
|
.expect(201);
|
|
|
|
expect(login.body.data).toMatchObject({
|
|
accessToken: expect.any(String),
|
|
user: {
|
|
id: expect.any(Number),
|
|
tiktokOpenId: 'open-user-1',
|
|
nickname: 'Tester',
|
|
},
|
|
isNewUser: true,
|
|
});
|
|
|
|
const token = login.body.data.accessToken;
|
|
const drama = await request(app.getHttpServer())
|
|
.post('/api/admin/v1/dramas')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
title: 'Paid Drama',
|
|
coverUrl: 'https://cdn.example.com/paid.jpg',
|
|
type: 'paid',
|
|
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: 'Paid Episode',
|
|
coverUrl: 'https://cdn.example.com/paid-ep.jpg',
|
|
videoUrl: 'https://cdn.example.com/paid-ep.mp4',
|
|
durationSeconds: 300,
|
|
isPaid: true,
|
|
unlockPrice: 199,
|
|
status: 'published',
|
|
})
|
|
.expect(201);
|
|
|
|
await request(app.getHttpServer())
|
|
.get(`/api/app/v1/episodes/${episode.body.data.id}/play`)
|
|
.expect(401);
|
|
|
|
const lockedPlay = await request(app.getHttpServer())
|
|
.get(`/api/app/v1/episodes/${episode.body.data.id}/play`)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
|
|
expect(lockedPlay.body.data).toMatchObject({
|
|
episodeId: episode.body.data.id,
|
|
isLocked: true,
|
|
videoUrl: null,
|
|
});
|
|
|
|
const order = await request(app.getHttpServer())
|
|
.post('/api/app/v1/orders')
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.send({ episodeId: episode.body.data.id })
|
|
.expect(201);
|
|
|
|
expect(order.body.data).toMatchObject({
|
|
orderNo: expect.any(String),
|
|
status: 'pending',
|
|
amount: 199,
|
|
});
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/webhooks/tiktok/payments')
|
|
.send({
|
|
orderNo: order.body.data.orderNo,
|
|
providerTransactionId: 'txn-1',
|
|
status: 'paid',
|
|
})
|
|
.expect(201);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/webhooks/tiktok/payments')
|
|
.send({
|
|
orderNo: order.body.data.orderNo,
|
|
providerTransactionId: 'txn-1',
|
|
status: 'paid',
|
|
})
|
|
.expect(201);
|
|
|
|
const unlockedPlay = await request(app.getHttpServer())
|
|
.get(`/api/app/v1/episodes/${episode.body.data.id}/play`)
|
|
.set('Authorization', `Bearer ${token}`)
|
|
.expect(200);
|
|
|
|
expect(unlockedPlay.body.data).toMatchObject({
|
|
episodeId: episode.body.data.id,
|
|
isLocked: false,
|
|
videoUrl: 'https://cdn.example.com/paid-ep.mp4',
|
|
});
|
|
});
|
|
|
|
it('records, updates, and isolates mini app playback history', async () => {
|
|
const firstLogin = await request(app.getHttpServer())
|
|
.post('/api/app/v1/auth/tiktok-login')
|
|
.send({
|
|
code: 'history-user-code-1',
|
|
mockOpenId: 'history-user-1',
|
|
nickname: 'History User 1',
|
|
})
|
|
.expect(201);
|
|
const firstToken = firstLogin.body.data.accessToken;
|
|
|
|
const secondLogin = await request(app.getHttpServer())
|
|
.post('/api/app/v1/auth/tiktok-login')
|
|
.send({
|
|
code: 'history-user-code-2',
|
|
mockOpenId: 'history-user-2',
|
|
nickname: 'History User 2',
|
|
})
|
|
.expect(201);
|
|
const secondToken = secondLogin.body.data.accessToken;
|
|
|
|
const drama = await request(app.getHttpServer())
|
|
.post('/api/admin/v1/dramas')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
title: 'History Drama',
|
|
coverUrl: 'https://cdn.example.com/history-drama.jpg',
|
|
type: 'history',
|
|
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: 'History Episode',
|
|
coverUrl: 'https://cdn.example.com/history-episode.jpg',
|
|
videoUrl: 'https://cdn.example.com/history-episode.mp4',
|
|
durationSeconds: 300,
|
|
status: 'published',
|
|
publishStatus: 'published',
|
|
reviewStatus: 'approved',
|
|
})
|
|
.expect(201);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/app/v1/history')
|
|
.send({
|
|
episodeId: episode.body.data.id,
|
|
progressSeconds: 86,
|
|
completed: false,
|
|
occurredAt: '2026-07-02T10:00:00.000Z',
|
|
})
|
|
.expect(401);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/app/v1/history')
|
|
.set('Authorization', `Bearer ${firstToken}`)
|
|
.send({
|
|
episodeId: episode.body.data.id,
|
|
progressSeconds: 86,
|
|
completed: false,
|
|
occurredAt: '2026-07-02T10:00:00.000Z',
|
|
})
|
|
.expect(201);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/app/v1/history')
|
|
.set('Authorization', `Bearer ${firstToken}`)
|
|
.send({
|
|
episodeId: episode.body.data.id,
|
|
progressSeconds: 301,
|
|
completed: false,
|
|
occurredAt: '2026-07-02T10:05:00.000Z',
|
|
})
|
|
.expect(201);
|
|
|
|
const firstHistory = await request(app.getHttpServer())
|
|
.get('/api/app/v1/history')
|
|
.set('Authorization', `Bearer ${firstToken}`)
|
|
.expect(200);
|
|
|
|
expect(firstHistory.body.data).toMatchObject({
|
|
pagination: { page: 1, pageSize: 20, total: 1 },
|
|
list: [
|
|
{
|
|
dramaId: drama.body.data.id,
|
|
episodeId: episode.body.data.id,
|
|
episodeNo: 1,
|
|
title: 'History Episode',
|
|
coverUrl: 'https://cdn.example.com/history-episode.jpg',
|
|
progressSeconds: 300,
|
|
durationSeconds: 300,
|
|
completed: true,
|
|
lastPlayedAt: '2026-07-02T10:05:00.000Z',
|
|
dramaTitle: 'History Drama',
|
|
dramaCoverUrl: 'https://cdn.example.com/history-drama.jpg',
|
|
},
|
|
],
|
|
});
|
|
|
|
const secondHistory = await request(app.getHttpServer())
|
|
.get('/api/app/v1/history')
|
|
.set('Authorization', `Bearer ${secondToken}`)
|
|
.expect(200);
|
|
|
|
expect(secondHistory.body.data).toMatchObject({
|
|
pagination: { page: 1, pageSize: 20, total: 0 },
|
|
list: [],
|
|
});
|
|
});
|
|
|
|
it('rejects playback history for unavailable episodes', async () => {
|
|
const login = await request(app.getHttpServer())
|
|
.post('/api/app/v1/auth/tiktok-login')
|
|
.send({
|
|
code: 'history-user-code-3',
|
|
mockOpenId: 'history-user-3',
|
|
})
|
|
.expect(201);
|
|
|
|
const drama = await request(app.getHttpServer())
|
|
.post('/api/admin/v1/dramas')
|
|
.set('Authorization', `Bearer ${adminToken}`)
|
|
.send({
|
|
title: 'Draft History Drama',
|
|
coverUrl: 'https://cdn.example.com/draft-history-drama.jpg',
|
|
type: 'history',
|
|
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: 'Draft History Episode',
|
|
coverUrl: 'https://cdn.example.com/draft-history-episode.jpg',
|
|
durationSeconds: 120,
|
|
status: 'draft',
|
|
publishStatus: 'draft',
|
|
reviewStatus: 'not_submitted',
|
|
})
|
|
.expect(201);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/api/app/v1/history')
|
|
.set('Authorization', `Bearer ${login.body.data.accessToken}`)
|
|
.send({
|
|
episodeId: episode.body.data.id,
|
|
progressSeconds: 10,
|
|
})
|
|
.expect(404);
|
|
});
|
|
});
|