import { Test, TestingModule } from '@nestjs/testing' import { getModelToken } from '@nestjs/mongoose' import { PaymentController } from './payment.controller' import { WechatPayService } from './wechat-pay.service' import { QuotaService } from '../user/quota.service' import { PricingService } from '../schemas/pricing.service' describe('PaymentController', () => { let controller: PaymentController let mockUserModel: any let mockOrderModel: any let mockWechatPay: any let mockQuotaService: any let mockPricingService: any const mockUserId = '507f1f77bcf86cd799439011' beforeEach(async () => { mockUserModel = { findById: jest.fn(), findByIdAndUpdate: jest.fn(), create: jest.fn(), } mockOrderModel = { findOne: jest.fn(), create: jest.fn(), } mockWechatPay = { nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/abc123' }), jsapiPay: jest.fn().mockResolvedValue({ paySign: 'mock-sign', nonceStr: 'mock-nonce', package: 'prepay_id=mock', timeStamp: '123456', signType: 'RSA' }), queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS', transaction_id: 'wx123' }), } mockQuotaService = { grantCredits: jest.fn().mockResolvedValue(undefined), setPlanQuota: jest.fn().mockResolvedValue(undefined), } mockPricingService = { getConfig: jest.fn().mockResolvedValue({ interview: { pricePerSession: 600, creditsPerPurchase: 1 }, resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 }, resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 }, plans: { growth: { price: 1990, durationDays: 30, gravityPerMonth: 80, credits: { interview: 999, resumeOptimize: 20, resumeDownload: 10 }, features: [] }, sprint: { price: 4990, durationDays: 30, gravityPerMonth: 200, credits: { interview: 999, resumeOptimize: 50, resumeDownload: 30 }, features: [] }, }, }), } const module: TestingModule = await Test.createTestingModule({ controllers: [PaymentController], providers: [ { provide: getModelToken('User'), useValue: mockUserModel }, { provide: getModelToken('PaymentOrder'), useValue: mockOrderModel }, { provide: getModelToken('GravityTransaction'), useValue: { create: jest.fn() } }, { provide: WechatPayService, useValue: mockWechatPay }, { provide: QuotaService, useValue: mockQuotaService }, { provide: PricingService, useValue: mockPricingService }, ], }).compile() controller = module.get(PaymentController) }) afterEach(() => { jest.clearAllMocks() }) describe('create', () => { it('should throw for invalid plan', async () => { await expect(controller.create(mockUserId, 'invalid')) .rejects.toThrow('无效套餐') }) it('should throw if user not found', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) await expect(controller.create(mockUserId, 'growth')) .rejects.toThrow('用户不存在') }) it('should throw if user already has plan', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'growth' }) }) await expect(controller.create(mockUserId, 'growth')) .rejects.toThrow('已是会员') }) it('should create a growth order', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) }) mockOrderModel.create.mockResolvedValue({}) const result = await controller.create(mockUserId, 'growth') expect(result).toHaveProperty('codeUrl') expect(result).toHaveProperty('outTradeNo') expect(mockWechatPay.nativePay).toHaveBeenCalled() expect(mockOrderModel.create).toHaveBeenCalled() }) it('should create a sprint order', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) }) mockOrderModel.create.mockResolvedValue({}) const result = await controller.create(mockUserId, 'sprint') expect(result).toHaveProperty('codeUrl') expect(mockWechatPay.nativePay).toHaveBeenCalled() }) }) describe('jsapi', () => { it('should throw if no wxOpenid', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: null }) }) await expect(controller.jsapi(mockUserId, 'growth')) .rejects.toThrow('未绑定微信') }) it('should return JSAPI pay params', async () => { mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: 'mock-openid', phone: '13800138000' }) }) mockOrderModel.create.mockResolvedValue({}) const result = await controller.jsapi(mockUserId, 'growth') expect(result).toHaveProperty('paySign') expect(result).toHaveProperty('outTradeNo') }) }) describe('checkOrder', () => { it('should throw if order not found for user', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) await expect(controller.checkOrder('no-such-order', mockUserId)) .rejects.toThrow('订单不存在') }) it('should return order status', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth', type: 'membership' }) }) const result = await controller.checkOrder('ORD123', mockUserId) expect(result).toEqual({ status: 'pending', plan: 'growth', type: 'membership' }) expect(mockOrderModel.findOne).toHaveBeenCalledWith({ outTradeNo: 'ORD123', userId: mockUserId }) }) }) describe('activate', () => { it('should throw if order not found for user', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) await expect(controller.activate(mockUserId, 'ORD123')) .rejects.toThrow('订单不存在') }) it('should throw if payment not completed', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth' }) }) await expect(controller.activate(mockUserId, 'ORD123')) .rejects.toThrow('支付未完成') }) it('should activate growth plan', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'growth', type: 'membership' }) }) const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, gravity: 0, freeOptimizeUsed: 0, save: jest.fn().mockResolvedValue(true) } mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) const result = await controller.activate(mockUserId, 'ORD123') expect(result.success).toBe(true) expect(result.plan).toBe('growth') expect(mockUser.save).toHaveBeenCalled() expect(mockUser.plan).toBe('growth') expect(mockUser.gravity).toBe(80) expect(mockUser.freeOptimizeUsed).toBe(3) }) it('should activate sprint plan', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'sprint', type: 'membership' }) }) const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, interviewCredits: 1, resumeOptimizeCredits: 0, resumeDownloadCredits: 0, freeOptimizeUsed: 0, save: jest.fn().mockResolvedValue(true) } mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) }) const result = await controller.activate(mockUserId, 'ORD123') expect(result.success).toBe(true) expect(result.plan).toBe('sprint') expect(mockUser.sprintRemaining).toBe(10) expect(mockUser.save).toHaveBeenCalled() }) }) describe('query', () => { it('should throw if order not owned by user', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) }) await expect(controller.query('ORD123', mockUserId)) .rejects.toThrow('订单不存在') }) it('should query WeChat for order status', async () => { mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId }) }) const result = await controller.query('ORD123', mockUserId) expect(result).toHaveProperty('trade_state', 'SUCCESS') expect(mockWechatPay.queryOrder).toHaveBeenCalledWith('ORD123') }) }) })