import request from 'supertest' import { Test, TestingModule } from '@nestjs/testing' import { INestApplication, ValidationPipe } from '@nestjs/common' import { getModelToken } from '@nestjs/mongoose' import { JwtService } from '@nestjs/jwt' import { WechatPayService } from '../src/modules/payment/wechat-pay.service' import { AppModule } from '../src/app.module' function makeChain(execValue: any = null) { const chain: Record = {} const methods = ['find', 'findOne', 'findById', 'findByIdAndUpdate', 'sort', 'skip', 'limit', 'select', 'lean', 'countDocuments'] for (const m of methods) { chain[m] = jest.fn(() => chain) } chain.exec = jest.fn().mockResolvedValue(execValue) chain.save = jest.fn().mockResolvedValue(true) return chain } function mockModel(extra: Record = {}, defaultExecValue: any = null) { const chain = makeChain(defaultExecValue) return { find: jest.fn(() => chain), findOne: jest.fn(() => chain), findById: jest.fn(() => chain), findByIdAndUpdate: jest.fn(() => chain), create: jest.fn(), countDocuments: jest.fn(() => chain), ...extra, } } describe('API Integration (e2e)', () => { let app: INestApplication let authToken: string beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(WechatPayService).useValue({ nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/test' }), jsapiPay: jest.fn().mockResolvedValue({ paySign: 'test', nonceStr: 'test', package: 'test', timeStamp: '0', signType: 'RSA' }), queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS' }), verifyAndDecrypt: jest.fn().mockReturnValue({ out_trade_no: 'TEST', transaction_id: 'wx_test' }), }) .overrideProvider(getModelToken('User')).useValue(mockModel({ countDocuments: jest.fn().mockResolvedValue(0) })) .overrideProvider(getModelToken('Interview')).useValue(mockModel({}, [])) .overrideProvider(getModelToken('PaymentOrder')).useValue(mockModel({}, null)) .overrideProvider(getModelToken('Progress')).useValue(mockModel({}, null)) .overrideProvider(getModelToken('CompanyBank')).useValue(mockModel()) .overrideProvider(getModelToken('Contribution')).useValue(mockModel()) .overrideProvider(getModelToken('SiteConfig')).useValue(mockModel()) .overrideProvider(getModelToken('Resume')).useValue(mockModel()) .compile() app = moduleFixture.createNestApplication() app.setGlobalPrefix('api') app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })) await app.init() const jwtService = app.get(JwtService) authToken = jwtService.sign({ userId: '507f1f77bcf86cd799439011', phone: '13800138000' }) }) afterAll(async () => { await app.close() }) afterEach(() => { jest.clearAllMocks() }) describe('Unauthenticated access', () => { it('GET /api/user/info should return 401', async () => { const res = await request(app.getHttpServer()).get('/api/user/info') expect(res.status).toBe(401) }) it('POST /api/user/send-code should be public', async () => { const res = await request(app.getHttpServer()) .post('/api/user/send-code') .send({ phone: '13800138000' }) expect(res.status).toBe(201) }) it('POST /api/payment/create should return 401', async () => { const res = await request(app.getHttpServer()) .post('/api/payment/create') .send({ plan: 'growth' }) expect(res.status).toBe(401) }) it('GET /api/progress should return 401', async () => { const res = await request(app.getHttpServer()).get('/api/progress') expect(res.status).toBe(401) }) it('POST /api/progress/checkin should return 401', async () => { const res = await request(app.getHttpServer()).post('/api/progress/checkin') expect(res.status).toBe(401) }) }) describe('Authenticated access', () => { const headers = () => ({ Authorization: `Bearer ${authToken}` }) it('POST /api/payment/create should validate plan', async () => { const res = await request(app.getHttpServer()) .post('/api/payment/create') .set(headers()) .send({ plan: 'invalid' }) expect(res.status).toBe(400) }) it('POST /api/payment/jsapi should handle missing wxOpenid', async () => { const userModel = app.get(getModelToken('User')) const chain = makeChain({ plan: 'free', wxOpenid: null }) userModel.findById.mockReturnValueOnce(chain) const res = await request(app.getHttpServer()) .post('/api/payment/jsapi') .set(headers()) .send({ plan: 'growth' }) expect(res.status).toBe(400) }) it('GET /api/admin/check should check admin status', async () => { const userModel = app.get(getModelToken('User')) const chain = makeChain({ role: 'user' }) userModel.findById.mockReturnValueOnce(chain) const res = await request(app.getHttpServer()) .get('/api/admin/check') .set(headers()) expect(res.status).toBe(200) expect(res.body.isAdmin).toBe(false) }) it('POST /api/progress/redeem should validate item', async () => { const res = await request(app.getHttpServer()) .post('/api/progress/redeem') .set(headers()) .send({ item: 'nonexistent' }) expect(res.status).toBe(400) }) it('GET /api/progress should return default progress for new user', async () => { const progressModel = app.get(getModelToken('Progress')) const nullChain = makeChain(null) progressModel.findOne.mockReturnValueOnce(nullChain) progressModel.create.mockResolvedValue({ userId: '507f1f77bcf86cd799439011', totalInterviews: 0, completedInterviews: 0, streak: 0, points: 0, totalCheckins: 0, recentScores: [], streakHistory: [], lastCheckinDate: null, }) const res = await request(app.getHttpServer()) .get('/api/progress') .set(headers()) expect(res.status).toBe(200) expect(res.body).toHaveProperty('totalInterviews', 0) expect(res.body).toHaveProperty('points') }) }) describe('Payment flow', () => { const headers = () => ({ Authorization: `Bearer ${authToken}` }) it('POST /api/payment/check should verify order ownership', async () => { const orderModel = app.get(getModelToken('PaymentOrder')) const chain = makeChain(null) orderModel.findOne.mockReturnValueOnce(chain) const res = await request(app.getHttpServer()) .get('/api/payment/check/ORD123') .set(headers()) expect(res.status).toBe(404) }) }) })