package alipay_test import ( "context" "crypto/rand" "crypto/rsa" "crypto/sha256" "encoding/base64" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "crypto" "github.com/wangjia/pay/internal/provider" ali "github.com/wangjia/pay/internal/provider/alipay" ) // jiu 反馈波 item 1:Metadata["is_mobile"]=="1" 应走 TradeWapPay,产出与 // page(FAST_INSTANT_TRADE_PAY)不同的收银台跳转(product_code=QUICK_WAP_WAY)。 // TradeWapPay/TradePagePay 都是纯本地签名构造 URL(不出网),同 alipay_test.go // TestCreateRedirect 的用法,无需 httptest。 func TestCreateWapDiffersFromPage(t *testing.T) { appPriv, _, aliPub := genKeys(t) p := ali.New(buildClient(t, appPriv, aliPub)) page, err := p.Create(context.Background(), provider.CreateRequest{ OutTradeNo: "PAY-WAP-1", Subject: "Pro 年付", AmountMinor: 19900, Currency: "CNY", ReturnURL: "https://x/return", }) if err != nil { t.Fatalf("create page: %v", err) } wap, err := p.Create(context.Background(), provider.CreateRequest{ OutTradeNo: "PAY-WAP-1", Subject: "Pro 年付", AmountMinor: 19900, Currency: "CNY", ReturnURL: "https://x/return", Metadata: map[string]string{"is_mobile": "1"}, }) if err != nil { t.Fatalf("create wap: %v", err) } if page.RenderType != provider.RenderRedirect || wap.RenderType != provider.RenderRedirect { t.Fatalf("render_type: page=%v wap=%v, want both redirect", page.RenderType, wap.RenderType) } pageURL, _ := page.Payload["url"].(string) wapURL, _ := wap.Payload["url"].(string) if pageURL == wapURL { t.Fatalf("wap URL 应与 page URL 不同(不同 product_code/签名参数), got 相同: %q", pageURL) } if !strings.Contains(wapURL, "QUICK_WAP_WAY") { t.Fatalf("wap URL 应带 product_code=QUICK_WAP_WAY, got %q", wapURL) } if !strings.Contains(pageURL, "FAST_INSTANT_TRADE_PAY") { t.Fatalf("page URL 应带 product_code=FAST_INSTANT_TRADE_PAY, got %q", pageURL) } } // fakeAlipayPreCreate 返回一份用"支付宝侧"私钥签名的 alipay.trade.precreate 响应 // (与 alipay_refund_test.go 的 fakeAlipayRefund 同一套 fixture 手法)。 func fakeAlipayPreCreate(t *testing.T, aliPriv *rsa.PrivateKey, qrCode string) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { node := map[string]any{ "code": "10000", "msg": "Success", "out_trade_no": "PAY-QR-1", "qr_code": qrCode, } nodeJSON, _ := json.Marshal(node) h := sha256.Sum256(nodeJSON) sig, _ := rsa.SignPKCS1v15(rand.Reader, aliPriv, crypto.SHA256, h[:]) resp := map[string]any{ "alipay_trade_precreate_response": json.RawMessage(nodeJSON), "sign": base64.StdEncoding.EncodeToString(sig), } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) })) } // jiu 反馈波 item 2:Metadata["render"]=="qr" 应走 TradePreCreate(当面付), // 产出 render_type=qr,payload 三字段 {qr_content, display_amount, currency}。 func TestCreateQR(t *testing.T) { aliKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("gen ali key: %v", err) } ts := fakeAlipayPreCreate(t, aliKey, "https://qr.alipay.com/bax12345") defer ts.Close() p := ali.New(buildRefundClient(t, ts.URL, &aliKey.PublicKey)) // 复用 alipay_refund_test.go 的 sandbox-gateway client builder sess, err := p.Create(context.Background(), provider.CreateRequest{ OutTradeNo: "PAY-QR-1", Subject: "Pro 年付", AmountMinor: 19900, Currency: "CNY", Metadata: map[string]string{"render": "qr"}, }) if err != nil { t.Fatalf("create qr: %v", err) } if sess.RenderType != provider.RenderQR { t.Fatalf("render_type = %v, want qr", sess.RenderType) } if sess.ExpiresAt == nil { t.Fatal("qr session 应带 ExpiresAt(当面付默认 2 小时窗口)") } if sess.Payload["qr_content"] != "https://qr.alipay.com/bax12345" { t.Fatalf("payload.qr_content = %v", sess.Payload["qr_content"]) } if sess.Payload["display_amount"] != "199" { // money.Format 去尾零(19900 分 → "199") t.Fatalf("payload.display_amount = %v, want 199", sess.Payload["display_amount"]) } if sess.Payload["currency"] != "CNY" { t.Fatalf("payload.currency = %v, want CNY", sess.Payload["currency"]) } } // alipay.trade.precreate 被渠道拒绝(如金额超限)时 Create 应报错、不返回 Session。 func TestCreateQRChannelRejected(t *testing.T) { aliKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("gen ali key: %v", err) } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { node := map[string]any{"code": "40004", "msg": "Business Failed", "sub_code": "ACQ.INVALID_PARAMETER", "sub_msg": "参数无效"} nodeJSON, _ := json.Marshal(node) h := sha256.Sum256(nodeJSON) sig, _ := rsa.SignPKCS1v15(rand.Reader, aliKey, crypto.SHA256, h[:]) resp := map[string]any{ "alipay_trade_precreate_response": json.RawMessage(nodeJSON), "sign": base64.StdEncoding.EncodeToString(sig), } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) })) defer ts.Close() p := ali.New(buildRefundClient(t, ts.URL, &aliKey.PublicKey)) sess, err := p.Create(context.Background(), provider.CreateRequest{ OutTradeNo: "PAY-QR-2", Subject: "Pro 年付", AmountMinor: 19900, Currency: "CNY", Metadata: map[string]string{"render": "qr"}, }) if err == nil { t.Fatalf("want 渠道拒绝返回 error, got session=%+v", sess) } if sess != nil { t.Fatalf("渠道拒绝时不应返回 Session, got %+v", sess) } }