Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 

468 righe
14 KiB

  1. package controller
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "encoding/pem"
  7. "net/http"
  8. "net/http/httptest"
  9. "os"
  10. "testing"
  11. "github.com/QuantumNous/new-api/common"
  12. "github.com/QuantumNous/new-api/model"
  13. "github.com/QuantumNous/new-api/setting"
  14. "github.com/glebarez/sqlite"
  15. "github.com/gin-gonic/gin"
  16. "github.com/go-pay/gopay/wechat/v3"
  17. "github.com/stretchr/testify/assert"
  18. "github.com/stretchr/testify/require"
  19. "gorm.io/gorm"
  20. )
  21. func setupWechatControllerDB(t *testing.T) *gorm.DB {
  22. t.Helper()
  23. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
  24. require.NoError(t, err)
  25. sqlDB, _ := db.DB()
  26. sqlDB.SetMaxOpenConns(1)
  27. origDB := model.DB
  28. origLogDB := model.LOG_DB
  29. model.DB = db
  30. model.LOG_DB = db
  31. common.UsingSQLite = true
  32. common.RedisEnabled = false
  33. common.QuotaPerUnit = 500 * 1000.0
  34. require.NoError(t, db.AutoMigrate(&model.User{}, &model.TopUp{}, &model.Log{}))
  35. t.Cleanup(func() {
  36. model.DB = origDB
  37. model.LOG_DB = origLogDB
  38. sqlDB.Close()
  39. })
  40. return db
  41. }
  42. func createTestUser(t *testing.T, db *gorm.DB, id int, username string) *model.User {
  43. user := &model.User{
  44. Id: id,
  45. Username: username,
  46. Password: "hashed",
  47. Quota: 100000,
  48. Role: common.RoleCommonUser,
  49. Status: common.UserStatusEnabled,
  50. AffCode: "aff_test_" + username,
  51. }
  52. require.NoError(t, db.Create(user).Error)
  53. return user
  54. }
  55. func newTestContextWithUser(userID int) (*gin.Context, *httptest.ResponseRecorder) {
  56. gin.SetMode(gin.TestMode)
  57. w := httptest.NewRecorder()
  58. c, _ := gin.CreateTestContext(w)
  59. c.Set("id", userID)
  60. return c, w
  61. }
  62. func setupWechatTestRouter() *gin.Engine {
  63. gin.SetMode(gin.TestMode)
  64. r := gin.New()
  65. r.POST("/api/user/wechat/pay/amount", RequestWechatPayAmount)
  66. r.POST("/api/user/wechat/pay", RequestWechatPay)
  67. r.GET("/api/user/wechat/pay/status", WechatPayStatus)
  68. return r
  69. }
  70. // ========== RequestWechatPayAmount 测试 ==========
  71. func TestRequestWechatPayAmount_Success(t *testing.T) {
  72. // 此测试验证 getWechatPayMoney 核心计算逻辑
  73. // RequestWechatPayAmount 依赖 GetUserGroup 等完整数据库,在集成测试中覆盖
  74. origUnitPrice := setting.WechatPayUnitPrice
  75. setting.WechatPayUnitPrice = 7.0
  76. t.Cleanup(func() {
  77. setting.WechatPayUnitPrice = origUnitPrice
  78. })
  79. // 测试不同金额的计算
  80. result := getWechatPayMoney(10.0, "default")
  81. assert.InDelta(t, 70.0, result, 0.01)
  82. result = getWechatPayMoney(1.0, "default")
  83. assert.InDelta(t, 7.0, result, 0.01)
  84. result = getWechatPayMoney(100.0, "default")
  85. assert.InDelta(t, 700.0, result, 0.01)
  86. }
  87. func TestRequestWechatPayAmount_BelowMinTopup(t *testing.T) {
  88. setupWechatControllerDB(t)
  89. origMinTopUp := setting.WechatPayMinTopUp
  90. setting.WechatPayMinTopUp = 5
  91. t.Cleanup(func() {
  92. setting.WechatPayMinTopUp = origMinTopUp
  93. })
  94. body, _ := json.Marshal(WechatPayRequest{Amount: 3})
  95. req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay/amount", bytes.NewReader(body))
  96. req.Header.Set("Content-Type", "application/json")
  97. w := httptest.NewRecorder()
  98. c, _ := gin.CreateTestContext(w)
  99. c.Request = req
  100. c.Set("id", 400)
  101. RequestWechatPayAmount(c)
  102. var resp map[string]interface{}
  103. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  104. assert.Equal(t, "error", resp["message"])
  105. }
  106. func TestRequestWechatPayAmount_InvalidBody(t *testing.T) {
  107. setupWechatControllerDB(t)
  108. req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay/amount", bytes.NewReader([]byte("invalid")))
  109. req.Header.Set("Content-Type", "application/json")
  110. w := httptest.NewRecorder()
  111. c, _ := gin.CreateTestContext(w)
  112. c.Request = req
  113. c.Set("id", 400)
  114. RequestWechatPayAmount(c)
  115. var resp map[string]interface{}
  116. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  117. assert.Equal(t, "error", resp["message"])
  118. assert.Equal(t, "参数错误", resp["data"])
  119. }
  120. // ========== RequestWechatPay 测试 ==========
  121. func TestRequestWechatPay_NotConfigured(t *testing.T) {
  122. setupWechatControllerDB(t)
  123. // 确保微信支付未配置
  124. origAppID := setting.WechatPayAppID
  125. setting.WechatPayAppID = ""
  126. t.Cleanup(func() {
  127. setting.WechatPayAppID = origAppID
  128. })
  129. body, _ := json.Marshal(WechatPayRequest{Amount: 10})
  130. req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body))
  131. req.Header.Set("Content-Type", "application/json")
  132. w := httptest.NewRecorder()
  133. c, _ := gin.CreateTestContext(w)
  134. c.Request = req
  135. c.Set("id", 400)
  136. RequestWechatPay(c)
  137. var resp map[string]interface{}
  138. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  139. assert.Equal(t, "error", resp["message"])
  140. assert.Equal(t, "微信支付未配置", resp["data"])
  141. }
  142. func TestRequestWechatPay_ExceedsMaxAmount(t *testing.T) {
  143. setupWechatControllerDB(t)
  144. body, _ := json.Marshal(WechatPayRequest{Amount: 20000})
  145. req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body))
  146. req.Header.Set("Content-Type", "application/json")
  147. w := httptest.NewRecorder()
  148. c, _ := gin.CreateTestContext(w)
  149. c.Request = req
  150. c.Set("id", 400)
  151. RequestWechatPay(c)
  152. var resp map[string]interface{}
  153. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  154. assert.Equal(t, "error", resp["message"])
  155. assert.Contains(t, resp["data"], "不能大于 10000")
  156. }
  157. func TestRequestWechatPay_BelowMinAmount(t *testing.T) {
  158. setupWechatControllerDB(t)
  159. origMinTopUp := setting.WechatPayMinTopUp
  160. setting.WechatPayMinTopUp = 5
  161. t.Cleanup(func() {
  162. setting.WechatPayMinTopUp = origMinTopUp
  163. })
  164. body, _ := json.Marshal(WechatPayRequest{Amount: 3})
  165. req := httptest.NewRequest(http.MethodPost, "/api/user/wechat/pay", bytes.NewReader(body))
  166. req.Header.Set("Content-Type", "application/json")
  167. w := httptest.NewRecorder()
  168. c, _ := gin.CreateTestContext(w)
  169. c.Request = req
  170. c.Set("id", 400)
  171. RequestWechatPay(c)
  172. var resp map[string]interface{}
  173. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  174. assert.Equal(t, "error", resp["message"])
  175. }
  176. // ========== WechatPayStatus 测试 ==========
  177. func TestWechatPayStatus_Success(t *testing.T) {
  178. db := setupWechatControllerDB(t)
  179. user := createTestUser(t, db, 401, "wx_status_test")
  180. // 创建订单
  181. topUp := &model.TopUp{
  182. UserId: user.Id,
  183. Amount: 10,
  184. Money: 70.0,
  185. TradeNo: "wx_status_trade_001",
  186. PaymentMethod: PaymentMethodWechatPay,
  187. CreateTime: common.GetTimestamp(),
  188. Status: common.TopUpStatusSuccess,
  189. }
  190. require.NoError(t, db.Create(topUp).Error)
  191. req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=wx_status_trade_001", nil)
  192. w := httptest.NewRecorder()
  193. c, _ := gin.CreateTestContext(w)
  194. c.Request = req
  195. c.Set("id", user.Id)
  196. WechatPayStatus(c)
  197. assert.Equal(t, 200, w.Code)
  198. var resp map[string]interface{}
  199. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  200. assert.Equal(t, "success", resp["message"])
  201. data := resp["data"].(map[string]interface{})
  202. assert.Equal(t, common.TopUpStatusSuccess, data["status"])
  203. }
  204. func TestWechatPayStatus_OrderNotFound(t *testing.T) {
  205. setupWechatControllerDB(t)
  206. req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=nonexistent", nil)
  207. w := httptest.NewRecorder()
  208. c, _ := gin.CreateTestContext(w)
  209. c.Request = req
  210. c.Set("id", 401)
  211. WechatPayStatus(c)
  212. var resp map[string]interface{}
  213. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  214. assert.Equal(t, "error", resp["message"])
  215. assert.Equal(t, "订单不存在", resp["data"])
  216. }
  217. func TestWechatPayStatus_WrongUser(t *testing.T) {
  218. db := setupWechatControllerDB(t)
  219. createTestUser(t, db, 402, "wx_status_owner")
  220. // 用户 403 尝试查询 402 的订单
  221. createTestUser(t, db, 403, "wx_status_other")
  222. topUp := &model.TopUp{
  223. UserId: 402,
  224. Amount: 10,
  225. Money: 70.0,
  226. TradeNo: "wx_status_trade_wrong_user",
  227. PaymentMethod: PaymentMethodWechatPay,
  228. CreateTime: common.GetTimestamp(),
  229. Status: common.TopUpStatusPending,
  230. }
  231. require.NoError(t, db.Create(topUp).Error)
  232. req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status?trade_no=wx_status_trade_wrong_user", nil)
  233. w := httptest.NewRecorder()
  234. c, _ := gin.CreateTestContext(w)
  235. c.Request = req
  236. c.Set("id", 403) // 不同用户
  237. WechatPayStatus(c)
  238. var resp map[string]interface{}
  239. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  240. assert.Equal(t, "error", resp["message"])
  241. assert.Equal(t, "订单不存在", resp["data"]) // 不泄露订单存在信息
  242. }
  243. func TestWechatPayStatus_MissingTradeNo(t *testing.T) {
  244. setupWechatControllerDB(t)
  245. req := httptest.NewRequest(http.MethodGet, "/api/user/wechat/pay/status", nil)
  246. w := httptest.NewRecorder()
  247. c, _ := gin.CreateTestContext(w)
  248. c.Request = req
  249. c.Set("id", 401)
  250. WechatPayStatus(c)
  251. var resp map[string]interface{}
  252. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
  253. assert.Equal(t, "error", resp["message"])
  254. assert.Equal(t, "参数错误", resp["data"])
  255. }
  256. // ========== getWechatPayMoney 测试 ==========
  257. func TestGetWechatPayMoney(t *testing.T) {
  258. origUnitPrice := setting.WechatPayUnitPrice
  259. setting.WechatPayUnitPrice = 7.0
  260. t.Cleanup(func() {
  261. setting.WechatPayUnitPrice = origUnitPrice
  262. })
  263. tests := []struct {
  264. name string
  265. amount float64
  266. group string
  267. expected float64
  268. }{
  269. {"标准计算", 10, "default", 70.0},
  270. {"最小金额", 1, "default", 7.0},
  271. }
  272. for _, tt := range tests {
  273. t.Run(tt.name, func(t *testing.T) {
  274. result := getWechatPayMoney(tt.amount, tt.group)
  275. assert.InDelta(t, tt.expected, result, 0.01)
  276. })
  277. }
  278. }
  279. // ========== getWechatMinTopup 测试 ==========
  280. func TestGetWechatMinTopup(t *testing.T) {
  281. origMinTopUp := setting.WechatPayMinTopUp
  282. setting.WechatPayMinTopUp = 1
  283. t.Cleanup(func() {
  284. setting.WechatPayMinTopUp = origMinTopUp
  285. })
  286. result := getWechatMinTopup()
  287. assert.Equal(t, int64(1), result)
  288. }
  289. // ========== ResetWechatPayClient 测试 ==========
  290. func TestResetWechatPayClient(t *testing.T) {
  291. // 确保重置后客户端为 nil
  292. wechatPayClient = nil
  293. ResetWechatPayClient()
  294. assert.Nil(t, wechatPayClient)
  295. }
  296. func TestResetWechatPayClient_CallbackRegistered(t *testing.T) {
  297. // 验证 init() 注册了回调
  298. assert.NotNil(t, setting.OnWechatPayConfigChanged)
  299. }
  300. // ========== PaymentMethodWechatPay 常量测试 ==========
  301. func TestPaymentMethodWechatPay(t *testing.T) {
  302. assert.Equal(t, "wechat_pay", PaymentMethodWechatPay)
  303. }
  304. // ========== Base64 密钥解析测试 ==========
  305. func TestBase64PemDecoding(t *testing.T) {
  306. // 测试:PEM 文件整体 Base64 编码 → 解码后应能得到有效 PEM
  307. keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem"
  308. pubKeyPath := "C:/Users/28221/Downloads/pub_key.pem"
  309. // 读取原始文件
  310. origKeyBytes, err := os.ReadFile(keyPath)
  311. require.NoError(t, err, "读取私钥文件失败")
  312. t.Logf("原始私钥前30字符: %s", string(origKeyBytes[:30]))
  313. // 模拟 getWechatPayClient 的逻辑:base64 编码 → 存入配置 → 解码
  314. encoded := base64.StdEncoding.EncodeToString(origKeyBytes)
  315. t.Logf("Base64 编码后长度: %d", len(encoded))
  316. // 解码
  317. decoded, err := base64.StdEncoding.DecodeString(encoded)
  318. require.NoError(t, err, "Base64 解码失败")
  319. assert.Equal(t, string(origKeyBytes), string(decoded), "解码后应与原始一致")
  320. // 验证解码后确实是 PEM 格式
  321. assert.Contains(t, string(decoded), "-----BEGIN", "解码后应包含 PEM 头")
  322. t.Logf("解码后前30字符: %s", string(decoded[:30]))
  323. // 测试 gopay 能否接受这个格式
  324. mchID := "1602799864"
  325. serialNo := "1ABE61AF2D4FA785F2782320B001D8B2E5517840"
  326. apiV3Key := "Pr79nX6Ej4Xw4eReJ7KM7NhE4Hezec3y"
  327. client, err := wechat.NewClientV3(mchID, serialNo, apiV3Key, string(decoded))
  328. if err != nil {
  329. t.Errorf("NewClientV3 失败: %v", err)
  330. } else {
  331. t.Log("NewClientV3 成功")
  332. _ = client
  333. }
  334. // 读取公钥
  335. pubKeyBytes, err := os.ReadFile(pubKeyPath)
  336. require.NoError(t, err, "读取公钥文件失败")
  337. t.Logf("原始公钥前30字符: %s", string(pubKeyBytes[:30]))
  338. // Base64 编码公钥
  339. encodedPub := base64.StdEncoding.EncodeToString(pubKeyBytes)
  340. decodedPub, err := base64.StdEncoding.DecodeString(encodedPub)
  341. require.NoError(t, err)
  342. assert.Contains(t, string(decodedPub), "-----BEGIN", "解码后应包含 PEM 头")
  343. }
  344. func TestWrapAsPEM(t *testing.T) {
  345. // Case 1: PEM 格式输入应原样返回
  346. pemInput := "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkq\n-----END PRIVATE KEY-----"
  347. result := wrapAsPEM([]byte(pemInput), "PRIVATE KEY")
  348. assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
  349. // Case 2: 裸 DER 字节应自动包装
  350. rawDER := []byte{0x30, 0x82, 0x02, 0x5c}
  351. result = wrapAsPEM(rawDER, "PRIVATE KEY")
  352. assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
  353. assert.Contains(t, result, "-----END PRIVATE KEY-----")
  354. // Case 3: PEM 内层 base64 文本(双重编码场景)应正确解码为 DER 再包装
  355. // 模拟:原始 DER → base64 得到 "MIIEvAI..." 文本 → 作为输入
  356. innerB64 := base64.StdEncoding.EncodeToString(rawDER)
  357. result = wrapAsPEM([]byte(innerB64), "PRIVATE KEY")
  358. assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
  359. // 验证内层内容解码后等于原始 DER
  360. block, _ := pem.Decode([]byte(result))
  361. assert.Equal(t, rawDER, block.Bytes)
  362. // Case 4: 用实际私钥文件测试内层 base64 场景
  363. keyPath := "C:/Users/28221/Downloads/WXCertUtil/cert/1602799864_20260402_cert/apiclient_key.pem"
  364. if keyBytes, err := os.ReadFile(keyPath); err == nil {
  365. // 提取 PEM 内层的 base64 文本
  366. block, _ := pem.Decode(keyBytes)
  367. require.NotNil(t, block, "原始 PEM 解析失败")
  368. innerB64 := base64.StdEncoding.EncodeToString(block.Bytes)
  369. // 用内层 base64 文本作为输入
  370. result := wrapAsPEM([]byte(innerB64), "PRIVATE KEY")
  371. assert.Contains(t, result, "-----BEGIN PRIVATE KEY-----")
  372. // 验证 gopay 能接受
  373. _, err := wechat.NewClientV3("test_mch", "test_serial", "test_key", result)
  374. t.Logf("gopay NewClientV3 with inner-b64 input: err=%v", err)
  375. }
  376. }