Files
cyh-tk-backend/test/orders-playback.e2e-spec.ts
zhxiao1124 fca247c3f2 feat(dramas): 新增剧集搜索、推荐接口与管理筛选功能
1.  为后台剧集列表添加标题和推荐状态筛选参数
2.  新增前台推荐剧集列表接口和前置搜索接口
3.  添加可选布尔值解析工具方法
4.  新增对应接口的端到端测试用例
2026-07-02 17:26:08 +08:00

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.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/playback-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/playback-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/playback-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/playback-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/playback-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/playback-history')
.set('Authorization', `Bearer ${login.body.data.accessToken}`)
.send({
episodeId: episode.body.data.id,
progressSeconds: 10,
})
.expect(404);
});
});