|
|
@@ -0,0 +1,397 @@
|
|
|
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
|
+import { testClient } from 'hono/testing';
|
|
|
+import {
|
|
|
+ IntegrationTestDatabase,
|
|
|
+ setupIntegrationDatabaseHooksWithEntities
|
|
|
+} from '@d8d/shared-test-util';
|
|
|
+import { PaymentRoutes } from '../../src/routes/payment.routes.js';
|
|
|
+import { PaymentEntity } from '../../src/entities/payment.entity.js';
|
|
|
+import { PaymentStatus } from '../../src/entities/payment.types.js';
|
|
|
+import { UserEntity } from '@d8d/user-module';
|
|
|
+import { Role } from '@d8d/user-module';
|
|
|
+import { File } from '@d8d/file-module';
|
|
|
+import { JWTUtil } from '@d8d/shared-utils';
|
|
|
+import { config } from 'dotenv';
|
|
|
+import { resolve } from 'path';
|
|
|
+// 导入微信支付SDK用于模拟
|
|
|
+import WxPay from 'wechatpay-node-v3';
|
|
|
+
|
|
|
+// 在测试环境中加载环境变量
|
|
|
+config({ path: resolve(process.cwd(), '.env') });
|
|
|
+
|
|
|
+vi.mock('wechatpay-node-v3')
|
|
|
+
|
|
|
+// 设置集成测试钩子
|
|
|
+setupIntegrationDatabaseHooksWithEntities([PaymentEntity, UserEntity, File, Role])
|
|
|
+
|
|
|
+describe('支付API集成测试', () => {
|
|
|
+ let client: ReturnType<typeof testClient<typeof PaymentRoutes>>;
|
|
|
+ let testToken: string;
|
|
|
+ let testUser: UserEntity;
|
|
|
+ let testPayment: PaymentEntity;
|
|
|
+
|
|
|
+ beforeEach(async () => {
|
|
|
+ // 创建测试客户端
|
|
|
+ client = testClient(PaymentRoutes);
|
|
|
+
|
|
|
+ // 创建测试用户并生成token
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+
|
|
|
+ const userRepository = dataSource.getRepository(UserEntity);
|
|
|
+ testUser = userRepository.create({
|
|
|
+ username: `test_user_${Date.now()}`,
|
|
|
+ password: 'test_password',
|
|
|
+ nickname: '测试用户',
|
|
|
+ openid: 'oJy1-16IIG18XZLl7G32k1hHMUFg'
|
|
|
+ });
|
|
|
+ await userRepository.save(testUser);
|
|
|
+
|
|
|
+ // 生成测试用户的token
|
|
|
+ testToken = JWTUtil.generateToken({
|
|
|
+ id: testUser.id,
|
|
|
+ username: testUser.username,
|
|
|
+ roles: [{name:'user'}]
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建测试支付记录
|
|
|
+ const paymentRepository = dataSource.getRepository(PaymentEntity);
|
|
|
+ testPayment = paymentRepository.create({
|
|
|
+ externalOrderId: 1,
|
|
|
+ userId: testUser.id,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付',
|
|
|
+ paymentStatus: PaymentStatus.PENDING,
|
|
|
+ openid: testUser.openid!
|
|
|
+ });
|
|
|
+ await paymentRepository.save(testPayment);
|
|
|
+
|
|
|
+ // 设置微信支付SDK的全局mock
|
|
|
+ const mockWxPay = {
|
|
|
+ transactions_jsapi: vi.fn().mockResolvedValue({
|
|
|
+ package: 'prepay_id=wx_test_prepay_id_123456',
|
|
|
+ timeStamp: Math.floor(Date.now() / 1000).toString(),
|
|
|
+ nonceStr: 'test_nonce_string',
|
|
|
+ signType: 'RSA',
|
|
|
+ paySign: 'test_pay_sign'
|
|
|
+ }),
|
|
|
+ verifySign: vi.fn().mockResolvedValue(true),
|
|
|
+ decipher_gcm: vi.fn().mockReturnValue(JSON.stringify({
|
|
|
+ out_trade_no: `ORDER_${testPayment.id}_${Date.now()}`,
|
|
|
+ trade_state: 'SUCCESS',
|
|
|
+ transaction_id: 'test_transaction_id',
|
|
|
+ amount: {
|
|
|
+ total: 20000
|
|
|
+ }
|
|
|
+ })),
|
|
|
+ getSignature: vi.fn().mockReturnValue('mock_signature')
|
|
|
+ };
|
|
|
+
|
|
|
+ // 模拟PaymentService的wxPay实例
|
|
|
+ vi.mocked(WxPay).mockImplementation(() => mockWxPay as any);
|
|
|
+
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('POST /payment - 创建支付', () => {
|
|
|
+ it('应该成功创建支付订单', async () => {
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000, // 200元,单位分
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ console.debug('支付创建返回结果:', result);
|
|
|
+
|
|
|
+ expect(result).toHaveProperty('paymentId');
|
|
|
+ expect(result).toHaveProperty('timeStamp');
|
|
|
+ expect(result).toHaveProperty('nonceStr');
|
|
|
+ expect(result).toHaveProperty('package');
|
|
|
+ expect(result).toHaveProperty('signType');
|
|
|
+ expect(result).toHaveProperty('paySign');
|
|
|
+ expect(result).toHaveProperty('totalAmount'); // 验证新增的金额字段
|
|
|
+ expect(result.paymentId).toBeDefined();
|
|
|
+ expect(result.paymentId).not.toBe('undefined');
|
|
|
+ expect(result.totalAmount).toBe(20000); // 验证金额正确返回
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该拒绝未认证的请求', async () => {
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(401);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该验证外部订单存在性', async () => {
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: 99999, // 不存在的外部订单ID
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(500);
|
|
|
+ if (response.status === 500) {
|
|
|
+ const result = await response.json();
|
|
|
+ expect(result.message).toContain('支付记录不存在');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该验证支付金额匹配', async () => {
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 30000, // 金额不匹配
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(500);
|
|
|
+ if (response.status === 500) {
|
|
|
+ const result = await response.json();
|
|
|
+ expect(result.message).toContain('支付金额与记录金额不匹配');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该验证支付状态', async () => {
|
|
|
+ // 更新支付状态为已支付
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ const paymentRepository = dataSource.getRepository(PaymentEntity);
|
|
|
+ await paymentRepository.update(testPayment.id, {
|
|
|
+ paymentStatus: PaymentStatus.SUCCESS
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(500);
|
|
|
+ if (response.status === 500) {
|
|
|
+ const result = await response.json();
|
|
|
+ expect(result.message).toContain('支付状态不正确');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该拒绝没有openid的用户支付', async () => {
|
|
|
+ // 创建没有openid的测试用户
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ const userRepository = dataSource.getRepository(UserEntity);
|
|
|
+
|
|
|
+ const userWithoutOpenid = userRepository.create({
|
|
|
+ username: `test_user_no_openid_${Date.now()}`,
|
|
|
+ password: 'test_password',
|
|
|
+ nickname: '测试用户无OpenID',
|
|
|
+ openid: null
|
|
|
+ });
|
|
|
+ await userRepository.save(userWithoutOpenid);
|
|
|
+
|
|
|
+ const tokenWithoutOpenid = JWTUtil.generateToken({
|
|
|
+ id: userWithoutOpenid.id,
|
|
|
+ username: userWithoutOpenid.username,
|
|
|
+ roles: [{name:'user'}]
|
|
|
+ });
|
|
|
+
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${tokenWithoutOpenid}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+ if (response.status === 400) {
|
|
|
+ const result = await response.json();
|
|
|
+ expect(result.message).toContain('用户未绑定微信小程序');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('POST /payment/callback - 支付回调', () => {
|
|
|
+ it('应该成功处理支付成功回调', async () => {
|
|
|
+ const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
|
+ const nonce = Math.random().toString(36).substring(2, 15);
|
|
|
+
|
|
|
+ const callbackData = {
|
|
|
+ id: 'EV-201802251122332345',
|
|
|
+ create_time: '2018-06-08T10:34:56+08:00',
|
|
|
+ event_type: 'TRANSACTION.SUCCESS',
|
|
|
+ resource_type: 'encrypt-resource',
|
|
|
+ resource: {
|
|
|
+ algorithm: 'AEAD_AES_256_GCM',
|
|
|
+ ciphertext: 'encrypted_data',
|
|
|
+ nonce: 'random_nonce',
|
|
|
+ associated_data: 'associated_data'
|
|
|
+ },
|
|
|
+ summary: 'payment_success'
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client.payment.callback.$post({
|
|
|
+ json: callbackData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'wechatpay-timestamp': timestamp,
|
|
|
+ 'wechatpay-nonce': nonce,
|
|
|
+ 'wechatpay-signature': 'mock_signature_for_test',
|
|
|
+ 'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const result = await response.text();
|
|
|
+ expect(result).toBe('SUCCESS');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该处理支付失败回调', async () => {
|
|
|
+ const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
|
+ const nonce = Math.random().toString(36).substring(2, 15);
|
|
|
+
|
|
|
+ const callbackData = {
|
|
|
+ id: 'EV-201802251122332346',
|
|
|
+ create_time: '2018-06-08T10:34:56+08:00',
|
|
|
+ event_type: 'TRANSACTION.FAIL',
|
|
|
+ resource_type: 'encrypt-resource',
|
|
|
+ resource: {
|
|
|
+ algorithm: 'AEAD_AES_256_GCM',
|
|
|
+ ciphertext: 'encrypted_data',
|
|
|
+ nonce: 'random_nonce',
|
|
|
+ associated_data: 'associated_data'
|
|
|
+ },
|
|
|
+ summary: 'payment_failed'
|
|
|
+ };
|
|
|
+
|
|
|
+ const response = await client.payment.callback.$post({
|
|
|
+ json: callbackData
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'wechatpay-timestamp': timestamp,
|
|
|
+ 'wechatpay-nonce': nonce,
|
|
|
+ 'wechatpay-signature': 'mock_signature_for_test',
|
|
|
+ 'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const result = await response.text();
|
|
|
+ expect(result).toBe('SUCCESS');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ it('应该处理无效的回调数据', async () => {
|
|
|
+ const response = await client.payment.callback.$post({
|
|
|
+ json: { invalid: 'data' } as any
|
|
|
+ }, {
|
|
|
+ headers: {
|
|
|
+ 'wechatpay-timestamp': '1622456896',
|
|
|
+ 'wechatpay-nonce': 'random_nonce_string',
|
|
|
+ 'wechatpay-signature': 'signature_data',
|
|
|
+ 'wechatpay-serial': process.env.WECHAT_PLATFORM_CERT_SERIAL_NO || ''
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(400);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('支付状态流转测试', () => {
|
|
|
+ it('应该正确更新支付状态', async () => {
|
|
|
+ // 创建支付
|
|
|
+ const createResponse = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(createResponse.status).toBe(200);
|
|
|
+
|
|
|
+ // 验证支付状态已更新为处理中
|
|
|
+ const dataSource = await IntegrationTestDatabase.getDataSource();
|
|
|
+ const paymentRepository = dataSource.getRepository(PaymentEntity);
|
|
|
+ const updatedPayment = await paymentRepository.findOne({
|
|
|
+ where: { id: testPayment.id }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(updatedPayment?.paymentStatus).toBe(PaymentStatus.PROCESSING);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe('微信支付JSAPI参数生成测试', () => {
|
|
|
+ it('应该生成正确的支付参数格式', async () => {
|
|
|
+ const response = await client.payment.$post({
|
|
|
+ json: {
|
|
|
+ externalOrderId: testPayment.externalOrderId,
|
|
|
+ totalAmount: 20000,
|
|
|
+ description: '测试支付订单'
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Authorization': `Bearer ${testToken}`
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ expect(response.status).toBe(200);
|
|
|
+ if (response.status === 200) {
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ // 验证返回参数格式
|
|
|
+ expect(result.timeStamp).toMatch(/^\d+$/); // 时间戳应该是数字字符串
|
|
|
+ expect(result.nonceStr).toBeTruthy(); // 随机字符串应该存在
|
|
|
+ expect(result.package).toContain('prepay_id=');
|
|
|
+ expect(result.signType).toBe('RSA');
|
|
|
+ expect(result.paySign).toBeTruthy(); // 签名应该存在
|
|
|
+ expect(result.totalAmount).toBe(20000); // 验证金额字段正确返回
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|