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