wxpay_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709
  1. //go:build unit
  2. package provider
  3. import (
  4. "context"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "crypto/x509"
  8. "encoding/pem"
  9. "errors"
  10. "net/url"
  11. "strings"
  12. "testing"
  13. "github.com/Wei-Shaw/sub2api/internal/payment"
  14. "github.com/wechatpay-apiv3/wechatpay-go/core"
  15. "github.com/wechatpay-apiv3/wechatpay-go/services/payments"
  16. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/h5"
  17. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
  18. "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native"
  19. )
  20. // generateTestKeyPair returns a fresh RSA 2048 key pair as PEM strings.
  21. // The wechatpay-go SDK expects PKCS8 private keys and PKIX public keys.
  22. func generateTestKeyPair(t *testing.T) (privPEM, pubPEM string) {
  23. t.Helper()
  24. key, err := rsa.GenerateKey(rand.Reader, 2048)
  25. if err != nil {
  26. t.Fatalf("generate rsa key: %v", err)
  27. }
  28. privDER, err := x509.MarshalPKCS8PrivateKey(key)
  29. if err != nil {
  30. t.Fatalf("marshal pkcs8: %v", err)
  31. }
  32. pubDER, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
  33. if err != nil {
  34. t.Fatalf("marshal pkix: %v", err)
  35. }
  36. return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER})),
  37. string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubDER}))
  38. }
  39. func TestMapWxState(t *testing.T) {
  40. t.Parallel()
  41. tests := []struct {
  42. name string
  43. input string
  44. want string
  45. }{
  46. {
  47. name: "SUCCESS maps to paid",
  48. input: wxpayTradeStateSuccess,
  49. want: payment.ProviderStatusPaid,
  50. },
  51. {
  52. name: "REFUND maps to refunded",
  53. input: wxpayTradeStateRefund,
  54. want: payment.ProviderStatusRefunded,
  55. },
  56. {
  57. name: "CLOSED maps to failed",
  58. input: wxpayTradeStateClosed,
  59. want: payment.ProviderStatusFailed,
  60. },
  61. {
  62. name: "PAYERROR maps to failed",
  63. input: wxpayTradeStatePayError,
  64. want: payment.ProviderStatusFailed,
  65. },
  66. {
  67. name: "unknown state maps to pending",
  68. input: "NOTPAY",
  69. want: payment.ProviderStatusPending,
  70. },
  71. {
  72. name: "empty string maps to pending",
  73. input: "",
  74. want: payment.ProviderStatusPending,
  75. },
  76. }
  77. for _, tt := range tests {
  78. t.Run(tt.name, func(t *testing.T) {
  79. t.Parallel()
  80. got := mapWxState(tt.input)
  81. if got != tt.want {
  82. t.Errorf("mapWxState(%q) = %q, want %q", tt.input, got, tt.want)
  83. }
  84. })
  85. }
  86. }
  87. func TestWxSV(t *testing.T) {
  88. t.Parallel()
  89. tests := []struct {
  90. name string
  91. input *string
  92. want string
  93. }{
  94. {
  95. name: "nil pointer returns empty string",
  96. input: nil,
  97. want: "",
  98. },
  99. {
  100. name: "non-nil pointer returns value",
  101. input: strPtr("hello"),
  102. want: "hello",
  103. },
  104. {
  105. name: "pointer to empty string returns empty string",
  106. input: strPtr(""),
  107. want: "",
  108. },
  109. }
  110. for _, tt := range tests {
  111. t.Run(tt.name, func(t *testing.T) {
  112. t.Parallel()
  113. got := wxSV(tt.input)
  114. if got != tt.want {
  115. t.Errorf("wxSV() = %q, want %q", got, tt.want)
  116. }
  117. })
  118. }
  119. }
  120. func TestBuildWxpayTransactionMetadata(t *testing.T) {
  121. t.Parallel()
  122. tx := &payments.Transaction{
  123. Appid: strPtr("wx-app-id"),
  124. Mchid: strPtr("mch-id"),
  125. TradeState: strPtr(wxpayTradeStateSuccess),
  126. Amount: &payments.TransactionAmount{
  127. Currency: strPtr(wxpayCurrency),
  128. },
  129. }
  130. metadata := buildWxpayTransactionMetadata(tx)
  131. if metadata[wxpayMetadataAppID] != "wx-app-id" {
  132. t.Fatalf("appid = %q", metadata[wxpayMetadataAppID])
  133. }
  134. if metadata[wxpayMetadataMerchantID] != "mch-id" {
  135. t.Fatalf("mchid = %q", metadata[wxpayMetadataMerchantID])
  136. }
  137. if metadata[wxpayMetadataCurrency] != wxpayCurrency {
  138. t.Fatalf("currency = %q", metadata[wxpayMetadataCurrency])
  139. }
  140. if metadata[wxpayMetadataTradeState] != wxpayTradeStateSuccess {
  141. t.Fatalf("trade_state = %q", metadata[wxpayMetadataTradeState])
  142. }
  143. }
  144. func strPtr(s string) *string {
  145. return &s
  146. }
  147. func TestFormatPEM(t *testing.T) {
  148. t.Parallel()
  149. tests := []struct {
  150. name string
  151. key string
  152. keyType string
  153. want string
  154. }{
  155. {
  156. name: "raw key gets wrapped with headers",
  157. key: "MIIBIjANBgkqhki...",
  158. keyType: "PUBLIC KEY",
  159. want: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki...\n-----END PUBLIC KEY-----",
  160. },
  161. {
  162. name: "already formatted key is returned as-is",
  163. key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n-----END PRIVATE KEY-----",
  164. keyType: "PRIVATE KEY",
  165. want: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBg...\n-----END PRIVATE KEY-----",
  166. },
  167. {
  168. name: "key with leading/trailing whitespace is trimmed before check",
  169. key: " \n MIIBIjANBgkqhki... \n ",
  170. keyType: "PUBLIC KEY",
  171. want: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki...\n-----END PUBLIC KEY-----",
  172. },
  173. {
  174. name: "already formatted key with whitespace is trimmed and returned",
  175. key: " -----BEGIN RSA PRIVATE KEY-----\ndata\n-----END RSA PRIVATE KEY----- ",
  176. keyType: "RSA PRIVATE KEY",
  177. want: "-----BEGIN RSA PRIVATE KEY-----\ndata\n-----END RSA PRIVATE KEY-----",
  178. },
  179. }
  180. for _, tt := range tests {
  181. t.Run(tt.name, func(t *testing.T) {
  182. t.Parallel()
  183. got := formatPEM(tt.key, tt.keyType)
  184. if got != tt.want {
  185. t.Errorf("formatPEM(%q, %q) =\n%s\nwant:\n%s", tt.key, tt.keyType, got, tt.want)
  186. }
  187. })
  188. }
  189. }
  190. func TestNewWxpay(t *testing.T) {
  191. t.Parallel()
  192. privPEM, pubPEM := generateTestKeyPair(t)
  193. validConfig := map[string]string{
  194. "appId": "wx1234567890",
  195. "mchId": "1234567890",
  196. "privateKey": privPEM,
  197. "apiV3Key": "12345678901234567890123456789012", // exactly 32 bytes
  198. "publicKey": pubPEM,
  199. "publicKeyId": "PUB_KEY_ID_TEST",
  200. "certSerial": "SERIAL001",
  201. }
  202. // helper to clone and override config fields
  203. withOverride := func(overrides map[string]string) map[string]string {
  204. cfg := make(map[string]string, len(validConfig))
  205. for k, v := range validConfig {
  206. cfg[k] = v
  207. }
  208. for k, v := range overrides {
  209. cfg[k] = v
  210. }
  211. return cfg
  212. }
  213. tests := []struct {
  214. name string
  215. config map[string]string
  216. wantErr bool
  217. errSubstr string
  218. }{
  219. {
  220. name: "valid config succeeds",
  221. config: validConfig,
  222. wantErr: false,
  223. },
  224. {
  225. name: "missing appId",
  226. config: withOverride(map[string]string{"appId": ""}),
  227. wantErr: true,
  228. errSubstr: "appId",
  229. },
  230. {
  231. name: "missing mchId",
  232. config: withOverride(map[string]string{"mchId": ""}),
  233. wantErr: true,
  234. errSubstr: "mchId",
  235. },
  236. {
  237. name: "missing privateKey",
  238. config: withOverride(map[string]string{"privateKey": ""}),
  239. wantErr: true,
  240. errSubstr: "privateKey",
  241. },
  242. {
  243. name: "missing apiV3Key",
  244. config: withOverride(map[string]string{"apiV3Key": ""}),
  245. wantErr: true,
  246. errSubstr: "apiV3Key",
  247. },
  248. {
  249. name: "missing certSerial",
  250. config: withOverride(map[string]string{"certSerial": ""}),
  251. wantErr: true,
  252. errSubstr: "certSerial",
  253. },
  254. {
  255. name: "missing publicKey",
  256. config: withOverride(map[string]string{"publicKey": ""}),
  257. wantErr: true,
  258. errSubstr: "publicKey",
  259. },
  260. {
  261. name: "missing publicKeyId",
  262. config: withOverride(map[string]string{"publicKeyId": ""}),
  263. wantErr: true,
  264. errSubstr: "publicKeyId",
  265. },
  266. {
  267. name: "malformed privateKey PEM",
  268. config: withOverride(map[string]string{"privateKey": "not-a-valid-pem"}),
  269. wantErr: true,
  270. errSubstr: "WXPAY_CONFIG_INVALID_KEY",
  271. },
  272. {
  273. name: "malformed publicKey PEM",
  274. config: withOverride(map[string]string{"publicKey": "not-a-valid-pem"}),
  275. wantErr: true,
  276. errSubstr: "WXPAY_CONFIG_INVALID_KEY",
  277. },
  278. {
  279. name: "apiV3Key too short",
  280. config: withOverride(map[string]string{"apiV3Key": "short"}),
  281. wantErr: true,
  282. errSubstr: "WXPAY_CONFIG_INVALID_KEY_LENGTH",
  283. },
  284. {
  285. name: "apiV3Key too long",
  286. config: withOverride(map[string]string{"apiV3Key": "123456789012345678901234567890123"}), // 33 bytes
  287. wantErr: true,
  288. errSubstr: "WXPAY_CONFIG_INVALID_KEY_LENGTH",
  289. },
  290. }
  291. for _, tt := range tests {
  292. t.Run(tt.name, func(t *testing.T) {
  293. t.Parallel()
  294. got, err := NewWxpay("test-instance", tt.config)
  295. if tt.wantErr {
  296. if err == nil {
  297. t.Fatal("expected error, got nil")
  298. }
  299. if tt.errSubstr != "" && !strings.Contains(err.Error(), tt.errSubstr) {
  300. t.Errorf("error %q should contain %q", err.Error(), tt.errSubstr)
  301. }
  302. return
  303. }
  304. if err != nil {
  305. t.Fatalf("unexpected error: %v", err)
  306. }
  307. if got == nil {
  308. t.Fatal("expected non-nil Wxpay instance")
  309. }
  310. if got.instanceID != "test-instance" {
  311. t.Errorf("instanceID = %q, want %q", got.instanceID, "test-instance")
  312. }
  313. })
  314. }
  315. }
  316. func TestBuildWxpayResultURLPreservesResumeToken(t *testing.T) {
  317. t.Parallel()
  318. resultURL, err := buildWxpayResultURL("https://app.example.com/payment/result?order_id=42&resume_token=resume-42&status=success", payment.CreatePaymentRequest{
  319. OrderID: "sub2_42",
  320. PaymentType: payment.TypeWxpay,
  321. })
  322. if err != nil {
  323. t.Fatalf("buildWxpayResultURL returned error: %v", err)
  324. }
  325. parsed, err := url.Parse(resultURL)
  326. if err != nil {
  327. t.Fatalf("url.Parse returned error: %v", err)
  328. }
  329. query := parsed.Query()
  330. if parsed.Path != wxpayResultPath {
  331. t.Fatalf("path = %q, want %q", parsed.Path, wxpayResultPath)
  332. }
  333. if query.Get("resume_token") != "resume-42" {
  334. t.Fatalf("resume_token = %q, want %q", query.Get("resume_token"), "resume-42")
  335. }
  336. if query.Get("order_id") != "42" {
  337. t.Fatalf("order_id = %q, want %q", query.Get("order_id"), "42")
  338. }
  339. if query.Get("out_trade_no") != "sub2_42" {
  340. t.Fatalf("out_trade_no = %q, want %q", query.Get("out_trade_no"), "sub2_42")
  341. }
  342. }
  343. func TestResolveWxpayJSAPIAppID(t *testing.T) {
  344. t.Parallel()
  345. tests := []struct {
  346. name string
  347. config map[string]string
  348. want string
  349. }{
  350. {
  351. name: "prefers dedicated mp app id",
  352. config: map[string]string{
  353. "mpAppId": "wx-mp-app",
  354. "appId": "wx-merchant-app",
  355. },
  356. want: "wx-mp-app",
  357. },
  358. {
  359. name: "falls back to merchant app id",
  360. config: map[string]string{
  361. "appId": "wx-merchant-app",
  362. },
  363. want: "wx-merchant-app",
  364. },
  365. {
  366. name: "missing app ids returns empty",
  367. config: map[string]string{},
  368. want: "",
  369. },
  370. }
  371. for _, tt := range tests {
  372. t.Run(tt.name, func(t *testing.T) {
  373. t.Parallel()
  374. if got := ResolveWxpayJSAPIAppID(tt.config); got != tt.want {
  375. t.Fatalf("ResolveWxpayJSAPIAppID() = %q, want %q", got, tt.want)
  376. }
  377. })
  378. }
  379. }
  380. func TestResolveWxpayCreateMode(t *testing.T) {
  381. t.Parallel()
  382. tests := []struct {
  383. name string
  384. req payment.CreatePaymentRequest
  385. wantMode string
  386. wantErr string
  387. }{
  388. {
  389. name: "desktop uses native",
  390. req: payment.CreatePaymentRequest{},
  391. wantMode: wxpayModeNative,
  392. },
  393. {
  394. name: "mobile uses h5 when client ip is present",
  395. req: payment.CreatePaymentRequest{
  396. IsMobile: true,
  397. ClientIP: "203.0.113.10",
  398. },
  399. wantMode: wxpayModeH5,
  400. },
  401. {
  402. name: "mobile without client ip returns clear error",
  403. req: payment.CreatePaymentRequest{
  404. IsMobile: true,
  405. },
  406. wantErr: "requires client IP",
  407. },
  408. {
  409. name: "openid uses jsapi mode",
  410. req: payment.CreatePaymentRequest{
  411. OpenID: "openid-123",
  412. },
  413. wantMode: wxpayModeJSAPI,
  414. },
  415. }
  416. for _, tt := range tests {
  417. t.Run(tt.name, func(t *testing.T) {
  418. t.Parallel()
  419. got, err := resolveWxpayCreateMode(tt.req)
  420. if tt.wantErr != "" {
  421. if err == nil {
  422. t.Fatal("expected error, got nil")
  423. }
  424. if !strings.Contains(err.Error(), tt.wantErr) {
  425. t.Fatalf("error %q should contain %q", err.Error(), tt.wantErr)
  426. }
  427. return
  428. }
  429. if err != nil {
  430. t.Fatalf("unexpected error: %v", err)
  431. }
  432. if got != tt.wantMode {
  433. t.Fatalf("resolveWxpayCreateMode() = %q, want %q", got, tt.wantMode)
  434. }
  435. })
  436. }
  437. }
  438. func TestCreatePaymentWithOpenIDReturnsJSAPIResult(t *testing.T) {
  439. origJSAPIPrepay := wxpayJSAPIPrepayWithRequestPayment
  440. origNativePrepay := wxpayNativePrepay
  441. origH5Prepay := wxpayH5Prepay
  442. t.Cleanup(func() {
  443. wxpayJSAPIPrepayWithRequestPayment = origJSAPIPrepay
  444. wxpayNativePrepay = origNativePrepay
  445. wxpayH5Prepay = origH5Prepay
  446. })
  447. jsapiCalls := 0
  448. nativeCalls := 0
  449. h5Calls := 0
  450. wxpayJSAPIPrepayWithRequestPayment = func(ctx context.Context, svc jsapi.JsapiApiService, req jsapi.PrepayRequest) (*jsapi.PrepayWithRequestPaymentResponse, *core.APIResult, error) {
  451. jsapiCalls++
  452. if got := wxSV(req.Payer.Openid); got != "openid-123" {
  453. t.Fatalf("openid = %q, want %q", got, "openid-123")
  454. }
  455. if req.SceneInfo == nil || wxSV(req.SceneInfo.PayerClientIp) != "203.0.113.10" {
  456. t.Fatalf("scene_info payer_client_ip = %q, want %q", wxSV(req.SceneInfo.PayerClientIp), "203.0.113.10")
  457. }
  458. return &jsapi.PrepayWithRequestPaymentResponse{
  459. Appid: core.String("wx123"),
  460. TimeStamp: core.String("1712345678"),
  461. NonceStr: core.String("nonce-123"),
  462. Package: core.String("prepay_id=wx_prepay_123"),
  463. SignType: core.String("RSA"),
  464. PaySign: core.String("signed-payload"),
  465. }, nil, nil
  466. }
  467. wxpayNativePrepay = func(ctx context.Context, svc native.NativeApiService, req native.PrepayRequest) (*native.PrepayResponse, *core.APIResult, error) {
  468. nativeCalls++
  469. return &native.PrepayResponse{}, nil, nil
  470. }
  471. wxpayH5Prepay = func(ctx context.Context, svc h5.H5ApiService, req h5.PrepayRequest) (*h5.PrepayResponse, *core.APIResult, error) {
  472. h5Calls++
  473. return &h5.PrepayResponse{}, nil, nil
  474. }
  475. provider := &Wxpay{
  476. config: map[string]string{
  477. "appId": "wx123",
  478. "mchId": "mch123",
  479. },
  480. coreClient: &core.Client{},
  481. }
  482. resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
  483. OrderID: "sub2_88",
  484. Amount: "66.88",
  485. PaymentType: payment.TypeWxpay,
  486. NotifyURL: "https://merchant.example/payment/notify",
  487. OpenID: "openid-123",
  488. ClientIP: "203.0.113.10",
  489. })
  490. if err != nil {
  491. t.Fatalf("unexpected error: %v", err)
  492. }
  493. if jsapiCalls != 1 {
  494. t.Fatalf("jsapi prepay calls = %d, want 1", jsapiCalls)
  495. }
  496. if nativeCalls != 0 {
  497. t.Fatalf("native prepay calls = %d, want 0", nativeCalls)
  498. }
  499. if h5Calls != 0 {
  500. t.Fatalf("h5 prepay calls = %d, want 0", h5Calls)
  501. }
  502. if resp.ResultType != payment.CreatePaymentResultJSAPIReady {
  503. t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultJSAPIReady)
  504. }
  505. if resp.JSAPI == nil {
  506. t.Fatal("expected jsapi payload, got nil")
  507. }
  508. if resp.JSAPI.AppID != "wx123" {
  509. t.Fatalf("jsapi appId = %q, want %q", resp.JSAPI.AppID, "wx123")
  510. }
  511. if resp.JSAPI.TimeStamp != "1712345678" {
  512. t.Fatalf("jsapi timeStamp = %q, want %q", resp.JSAPI.TimeStamp, "1712345678")
  513. }
  514. if resp.JSAPI.NonceStr != "nonce-123" {
  515. t.Fatalf("jsapi nonceStr = %q, want %q", resp.JSAPI.NonceStr, "nonce-123")
  516. }
  517. if resp.JSAPI.Package != "prepay_id=wx_prepay_123" {
  518. t.Fatalf("jsapi package = %q, want %q", resp.JSAPI.Package, "prepay_id=wx_prepay_123")
  519. }
  520. if resp.JSAPI.SignType != "RSA" {
  521. t.Fatalf("jsapi signType = %q, want %q", resp.JSAPI.SignType, "RSA")
  522. }
  523. if resp.JSAPI.PaySign != "signed-payload" {
  524. t.Fatalf("jsapi paySign = %q, want %q", resp.JSAPI.PaySign, "signed-payload")
  525. }
  526. }
  527. func TestCreatePaymentMobileH5IncludesConfiguredSceneInfo(t *testing.T) {
  528. origJSAPIPrepay := wxpayJSAPIPrepayWithRequestPayment
  529. origNativePrepay := wxpayNativePrepay
  530. origH5Prepay := wxpayH5Prepay
  531. t.Cleanup(func() {
  532. wxpayJSAPIPrepayWithRequestPayment = origJSAPIPrepay
  533. wxpayNativePrepay = origNativePrepay
  534. wxpayH5Prepay = origH5Prepay
  535. })
  536. jsapiCalls := 0
  537. nativeCalls := 0
  538. h5Calls := 0
  539. wxpayJSAPIPrepayWithRequestPayment = func(ctx context.Context, svc jsapi.JsapiApiService, req jsapi.PrepayRequest) (*jsapi.PrepayWithRequestPaymentResponse, *core.APIResult, error) {
  540. jsapiCalls++
  541. return &jsapi.PrepayWithRequestPaymentResponse{}, nil, nil
  542. }
  543. wxpayNativePrepay = func(ctx context.Context, svc native.NativeApiService, req native.PrepayRequest) (*native.PrepayResponse, *core.APIResult, error) {
  544. nativeCalls++
  545. return &native.PrepayResponse{}, nil, nil
  546. }
  547. wxpayH5Prepay = func(ctx context.Context, svc h5.H5ApiService, req h5.PrepayRequest) (*h5.PrepayResponse, *core.APIResult, error) {
  548. h5Calls++
  549. if req.SceneInfo == nil {
  550. t.Fatal("expected scene_info, got nil")
  551. }
  552. if got := wxSV(req.SceneInfo.PayerClientIp); got != "203.0.113.10" {
  553. t.Fatalf("scene_info payer_client_ip = %q, want %q", got, "203.0.113.10")
  554. }
  555. if req.SceneInfo.H5Info == nil {
  556. t.Fatal("expected scene_info.h5_info, got nil")
  557. }
  558. if got := wxSV(req.SceneInfo.H5Info.Type); got != wxpayH5Type {
  559. t.Fatalf("scene_info.h5_info.type = %q, want %q", got, wxpayH5Type)
  560. }
  561. if got := wxSV(req.SceneInfo.H5Info.AppName); got != "Sub2API" {
  562. t.Fatalf("scene_info.h5_info.app_name = %q, want %q", got, "Sub2API")
  563. }
  564. if got := wxSV(req.SceneInfo.H5Info.AppUrl); got != "https://app.example.com" {
  565. t.Fatalf("scene_info.h5_info.app_url = %q, want %q", got, "https://app.example.com")
  566. }
  567. return &h5.PrepayResponse{
  568. H5Url: core.String("https://wx.tenpay.example/h5pay?prepay_id=1"),
  569. }, nil, nil
  570. }
  571. provider := &Wxpay{
  572. config: map[string]string{
  573. "appId": "wx123",
  574. "mchId": "mch123",
  575. "h5AppName": "Sub2API",
  576. "h5AppUrl": "https://app.example.com",
  577. },
  578. coreClient: &core.Client{},
  579. }
  580. resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
  581. OrderID: "sub2_99",
  582. Amount: "66.88",
  583. PaymentType: payment.TypeWxpay,
  584. Subject: "Balance Recharge",
  585. NotifyURL: "https://merchant.example/payment/notify",
  586. ReturnURL: "https://merchant.example/payment/result?resume_token=resume-99",
  587. ClientIP: "203.0.113.10",
  588. IsMobile: true,
  589. })
  590. if err != nil {
  591. t.Fatalf("unexpected error: %v", err)
  592. }
  593. if jsapiCalls != 0 {
  594. t.Fatalf("jsapi prepay calls = %d, want 0", jsapiCalls)
  595. }
  596. if nativeCalls != 0 {
  597. t.Fatalf("native prepay calls = %d, want 0", nativeCalls)
  598. }
  599. if h5Calls != 1 {
  600. t.Fatalf("h5 prepay calls = %d, want 1", h5Calls)
  601. }
  602. if !strings.Contains(resp.PayURL, "redirect_url=") {
  603. t.Fatalf("pay_url = %q, want redirect_url query appended", resp.PayURL)
  604. }
  605. }
  606. func TestCreatePaymentMobileH5ReturnsNoAuthErrorWithoutNativeFallback(t *testing.T) {
  607. origJSAPIPrepay := wxpayJSAPIPrepayWithRequestPayment
  608. origNativePrepay := wxpayNativePrepay
  609. origH5Prepay := wxpayH5Prepay
  610. t.Cleanup(func() {
  611. wxpayJSAPIPrepayWithRequestPayment = origJSAPIPrepay
  612. wxpayNativePrepay = origNativePrepay
  613. wxpayH5Prepay = origH5Prepay
  614. })
  615. jsapiCalls := 0
  616. nativeCalls := 0
  617. h5Calls := 0
  618. wxpayJSAPIPrepayWithRequestPayment = func(ctx context.Context, svc jsapi.JsapiApiService, req jsapi.PrepayRequest) (*jsapi.PrepayWithRequestPaymentResponse, *core.APIResult, error) {
  619. jsapiCalls++
  620. return &jsapi.PrepayWithRequestPaymentResponse{}, nil, nil
  621. }
  622. wxpayH5Prepay = func(ctx context.Context, svc h5.H5ApiService, req h5.PrepayRequest) (*h5.PrepayResponse, *core.APIResult, error) {
  623. h5Calls++
  624. return nil, nil, errors.New("NO_AUTH")
  625. }
  626. wxpayNativePrepay = func(ctx context.Context, svc native.NativeApiService, req native.PrepayRequest) (*native.PrepayResponse, *core.APIResult, error) {
  627. nativeCalls++
  628. return &native.PrepayResponse{
  629. CodeUrl: core.String("weixin://wxpay/bizpayurl?pr=fallback-native"),
  630. }, nil, nil
  631. }
  632. provider := &Wxpay{
  633. config: map[string]string{
  634. "appId": "wx123",
  635. "mchId": "mch123",
  636. },
  637. coreClient: &core.Client{},
  638. }
  639. resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
  640. OrderID: "sub2_100",
  641. Amount: "66.88",
  642. PaymentType: payment.TypeWxpay,
  643. Subject: "Balance Recharge",
  644. NotifyURL: "https://merchant.example/payment/notify",
  645. ClientIP: "203.0.113.10",
  646. IsMobile: true,
  647. })
  648. if err == nil {
  649. t.Fatal("expected no-auth error, got nil")
  650. }
  651. if jsapiCalls != 0 {
  652. t.Fatalf("jsapi prepay calls = %d, want 0", jsapiCalls)
  653. }
  654. if h5Calls != 1 {
  655. t.Fatalf("h5 prepay calls = %d, want 1", h5Calls)
  656. }
  657. if nativeCalls != 0 {
  658. t.Fatalf("native prepay calls = %d, want 0", nativeCalls)
  659. }
  660. if resp != nil {
  661. t.Fatalf("expected nil response, got %+v", resp)
  662. }
  663. if !strings.Contains(err.Error(), "NO_AUTH") {
  664. t.Fatalf("error = %v, want NO_AUTH", err)
  665. }
  666. }