package controller import ( "bytes" "encoding/base64" "encoding/json" "encoding/pem" "net/http" "net/http/httptest" "os" "testing" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/glebarez/sqlite" "github.com/gin-gonic/gin" "github.com/go-pay/gopay/wechat/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) func setupWechatControllerDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(1) origDB := model.DB origLogDB := model.LOG_DB model.DB = db model.LOG_DB = db common.UsingSQLite = true common.RedisEnabled = false common.QuotaPerUnit = 500 * 1000.0 require.NoError(t, db.AutoMigrate(&model.User{}, &model.TopUp{}, &model.Log{})) t.Cleanup(func() { model.DB = origDB model.LOG_DB = origLogDB sqlDB.Close() }) return db } func createTestUser(t *testing.T, db *gorm.DB, id int, username string) *model.User { user := &model.User{ Id: id, Username: username, Password: "hashed", Quota: 100000, Role: common.RoleCommonUser, Status: common.UserStatusEnabled, AffCode: "aff_test_" + username, } require.NoError(t, db.Create(user).Error) return user } func newTestContextWithUser(userID int) (*gin.Context, *httptest.ResponseRecorder) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Set("id", userID) return c, w } func setupWechatTestRouter() *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.POST("/api/user/wechat/pay/amount", RequestWechatPayAmount) r.POST("/api/user/wechat/pay", RequestWechatPay) r.GET("/api/user/wechat/pay/status", WechatPayStatus) return r } // ========== RequestWechatPayAmount 测试 ========== func TestRequestWechatPayAmount_Success(t *testing.T) { // 此测试验证 getWechatPayMoney 核心计算逻辑 // RequestWechatPayAmount 依赖 GetUserGroup 等完整数据库,在集成测试中覆盖 origUnitPrice := setting.WechatPayUnitPrice setting.WechatPayUnitPrice = 7.0 t.Cleanup(func() { setting.WechatPayUnitPrice = origUnitPrice }) // 测试不同金额的计算 result := getWechatPayMoney(10.0, "default") assert.InDelta(t, 70.0, result, 0.01) result = getWechatPayMoney(1.0, "default") assert.InDelta(t, 7.0, result, 0.01) result = getWechatPayMoney(100.0, "default") assert.InDelta(t, 700.0, result, 0.01) } func TestRequestWechatPayAmount_BelowMinTopup(t *testing.T) { setupWechatControllerDB(t) origMinTopUp := setting.WechatPayMinTopUp setting.WechatPayMinTopUp = 5 t.Cleanup(func() { setting.WechatPayMinTopUp = origMinTopUp }) body, _ := json.Marshal(WechatPayRequest{Amount: 3}) req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay/amount", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 400) RequestWechatPayAmount(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) } func TestRequestWechatPayAmount_InvalidBody(t *testing.T) { setupWechatControllerDB(t) req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay/amount", bytes.NewReader([]byte("invalid"))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 400) RequestWechatPayAmount(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Equal(t, "参数错误", resp["data"]) } // ========== RequestWechatPay 测试 ========== func TestRequestWechatPay_NotConfigured(t *testing.T) { setupWechatControllerDB(t) // 确保微信支付未配置 origAppID := setting.WechatPayAppID setting.WechatPayAppID = "" t.Cleanup(func() { setting.WechatPayAppID = origAppID }) body, _ := json.Marshal(WechatPayRequest{Amount: 10}) req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 400) RequestWechatPay(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Equal(t, "微信支付未配置", resp["data"]) } func TestRequestWechatPay_ExceedsMaxAmount(t *testing.T) { setupWechatControllerDB(t) body, _ := json.Marshal(WechatPayRequest{Amount: 20000}) req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 400) RequestWechatPay(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Contains(t, resp["data"], "不能大于 10000") } func TestRequestWechatPay_BelowMinAmount(t *testing.T) { setupWechatControllerDB(t) origMinTopUp := setting.WechatPayMinTopUp setting.WechatPayMinTopUp = 5 t.Cleanup(func() { setting.WechatPayMinTopUp = origMinTopUp }) body, _ := json.Marshal(WechatPayRequest{Amount: 3}) req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 400) RequestWechatPay(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) } // ========== WechatPayStatus 测试 ========== func TestWechatPayStatus_Success(t *testing.T) { db := setupWechatControllerDB(t) user := createTestUser(t, db, 401, "wx_status_test") // 创建订单 topUp := &model.TopUp{ UserId: user.Id, Amount: 10, Money: 70.0, TradeNo: "wx_status_trade_001", PaymentMethod: PaymentMethodWechatPay, CreateTime: common.GetTimestamp(), Status: common.TopUpStatusSuccess, } require.NoError(t, db.Create(topUp).Error) req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=wx_status_trade_001", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", user.Id) WechatPayStatus(c) assert.Equal(t, 200, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "success", resp["message"]) data := resp["data"].(map[string]interface{}) assert.Equal(t, common.TopUpStatusSuccess, data["status"]) } func TestWechatPayStatus_OrderNotFound(t *testing.T) { setupWechatControllerDB(t) req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=nonexistent", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 401) WechatPayStatus(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Equal(t, "订单不存在", resp["data"]) } func TestWechatPayStatus_WrongUser(t *testing.T) { db := setupWechatControllerDB(t) createTestUser(t, db, 402, "wx_status_owner") // 用户 403 尝试查询 402 的订单 createTestUser(t, db, 403, "wx_status_other") topUp := &model.TopUp{ UserId: 402, Amount: 10, Money: 70.0, TradeNo: "wx_status_trade_wrong_user", PaymentMethod: PaymentMethodWechatPay, CreateTime: common.GetTimestamp(), Status: common.TopUpStatusPending, } require.NoError(t, db.Create(topUp).Error) req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=wx_status_trade_wrong_user", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 403) // 不同用户 WechatPayStatus(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Equal(t, "订单不存在", resp["data"]) // 不泄露订单存在信息 } func TestWechatPayStatus_MissingTradeNo(t *testing.T) { setupWechatControllerDB(t) req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status", nil) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = req c.Set("id", 401) WechatPayStatus(c) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "error", resp["message"]) assert.Equal(t, "参数错误", resp["data"]) } // ========== getWechatPayMoney 测试 ========== func TestGetWechatPayMoney(t *testing.T) { origUnitPrice := setting.WechatPayUnitPrice setting.WechatPayUnitPrice = 7.0 t.Cleanup(func() { setting.WechatPayUnitPrice = origUnitPrice }) tests := []struct { name string amount float64 group string expected float64 }{ {"标准计算", 10, "default", 70.0}, {"最小金额", 1, "default", 7.0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := getWechatPayMoney(tt.amount, tt.group) assert.InDelta(t, tt.expected, result, 0.01) }) } } // ========== getWechatMinTopup 测试 ========== func TestGetWechatMinTopup(t *testing.T) { origMinTopUp := setting.WechatPayMinTopUp setting.WechatPayMinTopUp = 1 t.Cleanup(func() { setting.WechatPayMinTopUp = origMinTopUp }) result := getWechatMinTopup() assert.Equal(t, int64(1), result) } // ========== ResetWechatPayClient 测试 ========== func TestResetWechatPayClient(t *testing.T) { // 确保重置后客户端为 nil wechatPayClient = nil ResetWechatPayClient() assert.Nil(t, wechatPayClient) } func TestResetWechatPayClient_CallbackRegistered(t *testing.T) { // 验证 init() 注册了回调 assert.NotNil(t, setting.OnWechatPayConfigChanged) } // ========== PaymentMethodWechatPay 常量测试 ========== func TestPaymentMethodWechatPay(t *testing.T) { assert.Equal(t, "wechat_pay", PaymentMethodWechatPay) } // ========== Base64 密钥解析测试 ========== func TestBase64PemDecoding(t *testing.T) { // 测试:PEM 文件整体 Base64 编码 → 解码后应能得到有效 PEM keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem" pubKeyPath := "C:/Users/28221/Downloads/pub_key.pem" // 读取原始文件 origKeyBytes, err := os.ReadFile(keyPath) require.NoError(t, err, "读取私钥文件失败") t.Logf("原始私钥前30字符: %s", string(origKeyBytes[:30])) // 模拟 getWechatPayClient 的逻辑:base64 编码 → 存入配置 → 解码 encoded := base64.StdEncoding.EncodeToString(origKeyBytes) t.Logf("Base64 编码后长度: %d", len(encoded)) // 解码 decoded, err := base64.StdEncoding.DecodeString(encoded) require.NoError(t, err, "Base64 解码失败") assert.Equal(t, string(origKeyBytes), string(decoded), "解码后应与原始一致") // 验证解码后确实是 PEM 格式 assert.Contains(t, string(decoded), "-----BEGIN", "解码后应包含 PEM 头") t.Logf("解码后前30字符: %s", string(decoded[:30])) // 测试 gopay 能否接受这个格式 mchID := "1602799864" serialNo := "1ABE61AF2D4FA785F2782320B001D8B2E5517840" apiV3Key := "Pr79nX6Ej4Xw4eReJ7KM7NhE4Hezec3y" client, err := wechat.NewClientV3(mchID, serialNo, apiV3Key, string(decoded)) if err != nil { t.Errorf("NewClientV3 失败: %v", err) } else { t.Log("NewClientV3 成功") _ = client } // 读取公钥 pubKeyBytes, err := os.ReadFile(pubKeyPath) require.NoError(t, err, "读取公钥文件失败") t.Logf("原始公钥前30字符: %s", string(pubKeyBytes[:30])) // Base64 编码公钥 encodedPub := base64.StdEncoding.EncodeToString(pubKeyBytes) decodedPub, err := base64.StdEncoding.DecodeString(encodedPub) require.NoError(t, err) assert.Contains(t, string(decodedPub), "-----BEGIN", "解码后应包含 PEM 头") } func TestWrapAsPEM(t *testing.T) { // Case 1: PEM 格式输入应原样返回 pemInput := "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkq\n-----END PRIVATE KEY-----" result := wrapAsPEM([]byte(pemInput), "PRIVATE KEY") assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----") // Case 2: 裸 DER 字节应自动包装 rawDER := []byte{0x30, 0x82, 0x02, 0x5c} result = wrapAsPEM(rawDER, "PRIVATE KEY") assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----") assert.Contains(t, result, "-----END PRIVATE KEY-----") // Case 3: PEM 内层 base64 文本(双重编码场景)应正确解码为 DER 再包装 // 模拟:原始 DER → base64 得到 "MIIEvAI..." 文本 → 作为输入 innerB64 := base64.StdEncoding.EncodeToString(rawDER) result = wrapAsPEM([]byte(innerB64), "PRIVATE KEY") assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----") // 验证内层内容解码后等于原始 DER block, _ := pem.Decode([]byte(result)) assert.Equal(t, rawDER, block.Bytes) // Case 4: 用实际私钥文件测试内层 base64 场景 keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem" if keyBytes, err := os.ReadFile(keyPath); err == nil { // 提取 PEM 内层的 base64 文本 block, _ := pem.Decode(keyBytes) require.NotNil(t, block, "原始 PEM 解析失败") innerB64 := base64.StdEncoding.EncodeToString(block.Bytes) // 用内层 base64 文本作为输入 result := wrapAsPEM([]byte(innerB64), "PRIVATE KEY") assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----") // 验证 gopay 能接受 _, err := wechat.NewClientV3("test_mch", "test_serial", "test_key", result) t.Logf("gopay NewClientV3 with inner-b64 input: err=%v", err) } }