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); }); });