| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446 |
- //go:build unit
- package provider
- import (
- "context"
- "errors"
- "net/url"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- "github.com/smartwalle/alipay/v3"
- )
- func TestIsTradeNotExist(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- err error
- want bool
- }{
- {
- name: "nil error returns false",
- err: nil,
- want: false,
- },
- {
- name: "error containing ACQ.TRADE_NOT_EXIST returns true",
- err: errors.New("alipay: sub_code=ACQ.TRADE_NOT_EXIST, sub_msg=交易不存在"),
- want: true,
- },
- {
- name: "error not containing the code returns false",
- err: errors.New("alipay: sub_code=ACQ.SYSTEM_ERROR, sub_msg=系统错误"),
- want: false,
- },
- {
- name: "error with only partial match returns false",
- err: errors.New("ACQ.TRADE_NOT"),
- want: false,
- },
- {
- name: "error with exact constant value returns true",
- err: errors.New(alipayErrTradeNotExist),
- want: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got := isTradeNotExist(tt.err)
- if got != tt.want {
- t.Errorf("isTradeNotExist(%v) = %v, want %v", tt.err, got, tt.want)
- }
- })
- }
- }
- func TestNewAlipay(t *testing.T) {
- t.Parallel()
- validConfig := map[string]string{
- "appId": "2021001234567890",
- "privateKey": "MIIEvQIBADANBgkqhkiG9w0BAQEFAASC...",
- }
- // helper to clone and override config fields
- withOverride := func(overrides map[string]string) map[string]string {
- cfg := make(map[string]string, len(validConfig))
- for k, v := range validConfig {
- cfg[k] = v
- }
- for k, v := range overrides {
- cfg[k] = v
- }
- return cfg
- }
- tests := []struct {
- name string
- config map[string]string
- wantErr bool
- errSubstr string
- }{
- {
- name: "valid config succeeds",
- config: validConfig,
- wantErr: false,
- },
- {
- name: "missing appId",
- config: withOverride(map[string]string{"appId": ""}),
- wantErr: true,
- errSubstr: "appId",
- },
- {
- name: "missing privateKey",
- config: withOverride(map[string]string{"privateKey": ""}),
- wantErr: true,
- errSubstr: "privateKey",
- },
- {
- name: "nil config map returns error for appId",
- config: map[string]string{},
- wantErr: true,
- errSubstr: "appId",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got, err := NewAlipay("test-instance", tt.config)
- if tt.wantErr {
- if err == nil {
- t.Fatal("expected error, got nil")
- }
- if tt.errSubstr != "" && !strings.Contains(err.Error(), tt.errSubstr) {
- t.Errorf("error %q should contain %q", err.Error(), tt.errSubstr)
- }
- return
- }
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if got == nil {
- t.Fatal("expected non-nil Alipay instance")
- }
- if got.instanceID != "test-instance" {
- t.Errorf("instanceID = %q, want %q", got.instanceID, "test-instance")
- }
- })
- }
- }
- func TestCreateTradeUsesPagePayForDesktop(t *testing.T) {
- origPreCreate := alipayTradePreCreate
- origPagePay := alipayTradePagePay
- origWapPay := alipayTradeWapPay
- t.Cleanup(func() {
- alipayTradePreCreate = origPreCreate
- alipayTradePagePay = origPagePay
- alipayTradeWapPay = origWapPay
- })
- preCreateCalls := 0
- pagePayCalls := 0
- wapPayCalls := 0
- alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) {
- preCreateCalls++
- return nil, errors.New("merchant does not have FACE_TO_FACE_PAYMENT")
- }
- alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) {
- pagePayCalls++
- if param.OutTradeNo != "sub2_100" {
- t.Fatalf("out_trade_no = %q, want %q", param.OutTradeNo, "sub2_100")
- }
- if param.NotifyURL != "https://merchant.example.com/api/v1/payment/webhook/alipay" {
- t.Fatalf("notify_url = %q", param.NotifyURL)
- }
- return url.Parse("https://openapi.alipay.com/gateway.do?page-pay")
- }
- alipayTradeWapPay = func(client *alipay.Client, param alipay.TradeWapPay) (*url.URL, error) {
- wapPayCalls++
- return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay")
- }
- provider := &Alipay{}
- resp, err := provider.createDesktopTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{
- OrderID: "sub2_100",
- Amount: "88.00",
- Subject: "Balance recharge",
- }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if preCreateCalls != 1 {
- t.Fatalf("precreate calls = %d, want 1", preCreateCalls)
- }
- if pagePayCalls != 1 {
- t.Fatalf("page pay calls = %d, want 1", pagePayCalls)
- }
- if wapPayCalls != 0 {
- t.Fatalf("wap pay calls = %d, want 0", wapPayCalls)
- }
- if resp.PayURL == "" {
- t.Fatal("expected pay_url for desktop page pay")
- }
- // page.pay returns a checkout page URL, not a scannable QR payload —
- // it must never be exposed via QRCode (the frontend would render an
- // unscannable image from it).
- if resp.QRCode != "" {
- t.Fatalf("qr_code = %q, want empty for page pay", resp.QRCode)
- }
- }
- // When the provider instance is configured with paymentMode == "redirect",
- // the desktop flow must skip precreate and go straight to page.pay.
- func TestCreateTradeRedirectModeSkipsPrecreate(t *testing.T) {
- origPreCreate := alipayTradePreCreate
- origPagePay := alipayTradePagePay
- t.Cleanup(func() {
- alipayTradePreCreate = origPreCreate
- alipayTradePagePay = origPagePay
- })
- preCreateCalls := 0
- pagePayCalls := 0
- alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) {
- preCreateCalls++
- return &alipay.TradePreCreateRsp{
- Error: alipay.Error{Code: alipay.CodeSuccess},
- QRCode: "https://qr.alipay.example.com/precreate-token",
- }, nil
- }
- alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) {
- pagePayCalls++
- if param.ProductCode != alipayProductCodePagePay {
- t.Fatalf("product_code = %q, want %q", param.ProductCode, alipayProductCodePagePay)
- }
- return url.Parse("https://openapi.alipay.com/gateway.do?page-pay")
- }
- provider := &Alipay{
- config: map[string]string{"paymentMode": "redirect"},
- }
- resp, err := provider.createDesktopTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{
- OrderID: "sub2_103",
- Amount: "12.00",
- Subject: "Balance recharge",
- }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if preCreateCalls != 0 {
- t.Fatalf("precreate calls = %d, want 0 (redirect mode must skip precreate)", preCreateCalls)
- }
- if pagePayCalls != 1 {
- t.Fatalf("page pay calls = %d, want 1", pagePayCalls)
- }
- if resp.PayURL == "" {
- t.Fatal("expected pay_url for redirect mode")
- }
- if resp.QRCode != "" {
- t.Fatalf("qr_code = %q, want empty for redirect mode", resp.QRCode)
- }
- }
- func TestCreateTradeUsesWapPayForMobile(t *testing.T) {
- origWapPay := alipayTradeWapPay
- t.Cleanup(func() {
- alipayTradeWapPay = origWapPay
- })
- wapPayCalls := 0
- alipayTradeWapPay = func(client *alipay.Client, param alipay.TradeWapPay) (*url.URL, error) {
- wapPayCalls++
- if param.ReturnURL != "https://merchant.example.com/payment/result" {
- t.Fatalf("return_url = %q", param.ReturnURL)
- }
- return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay")
- }
- provider := &Alipay{}
- resp, err := provider.createWapTrade(&alipay.Client{}, payment.CreatePaymentRequest{
- OrderID: "sub2_101",
- Amount: "18.00",
- Subject: "Balance recharge",
- IsMobile: true,
- }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if wapPayCalls != 1 {
- t.Fatalf("wap pay calls = %d, want 1", wapPayCalls)
- }
- if resp.PayURL == "" {
- t.Fatal("expected pay_url for mobile wap pay")
- }
- }
- func TestCreatePaymentUsesPrecreateForMobileWhenEnabled(t *testing.T) {
- origPreCreate := alipayTradePreCreate
- origWapPay := alipayTradeWapPay
- t.Cleanup(func() {
- alipayTradePreCreate = origPreCreate
- alipayTradeWapPay = origWapPay
- })
- precreateCalls := 0
- wapPayCalls := 0
- alipayTradePreCreate = func(_ context.Context, _ *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) {
- precreateCalls++
- if param.OutTradeNo != "sub2_mobile_precreate" {
- t.Fatalf("out_trade_no = %q", param.OutTradeNo)
- }
- if param.ProductCode != alipayProductCodePreCreate {
- t.Fatalf("product_code = %q, want %q", param.ProductCode, alipayProductCodePreCreate)
- }
- return &alipay.TradePreCreateRsp{
- Error: alipay.Error{Code: alipay.CodeSuccess},
- QRCode: "https://qr.alipay.example.com/mobile-dynamic-token",
- }, nil
- }
- alipayTradeWapPay = func(_ *alipay.Client, _ alipay.TradeWapPay) (*url.URL, error) {
- wapPayCalls++
- return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay")
- }
- provider := &Alipay{client: &alipay.Client{}, config: map[string]string{}}
- resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
- OrderID: "sub2_mobile_precreate",
- Amount: "28.00",
- Subject: "Balance recharge",
- IsMobile: true,
- AlipayMobilePrecreate: true,
- })
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if precreateCalls != 1 || wapPayCalls != 0 {
- t.Fatalf("precreate calls = %d, wap calls = %d; want 1, 0", precreateCalls, wapPayCalls)
- }
- if resp.QRCode != "https://qr.alipay.example.com/mobile-dynamic-token" || resp.PayURL != "" {
- t.Fatalf("unexpected response: qr_code=%q pay_url=%q", resp.QRCode, resp.PayURL)
- }
- }
- func TestCreatePaymentKeepsWapPayForMobileWhenPrecreateDisabled(t *testing.T) {
- origPreCreate := alipayTradePreCreate
- origWapPay := alipayTradeWapPay
- t.Cleanup(func() {
- alipayTradePreCreate = origPreCreate
- alipayTradeWapPay = origWapPay
- })
- precreateCalls := 0
- wapPayCalls := 0
- alipayTradePreCreate = func(_ context.Context, _ *alipay.Client, _ alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) {
- precreateCalls++
- return nil, errors.New("unexpected precreate call")
- }
- alipayTradeWapPay = func(_ *alipay.Client, _ alipay.TradeWapPay) (*url.URL, error) {
- wapPayCalls++
- return url.Parse("https://openapi.alipay.com/gateway.do?wap-pay")
- }
- provider := &Alipay{client: &alipay.Client{}, config: map[string]string{}}
- resp, err := provider.CreatePayment(context.Background(), payment.CreatePaymentRequest{
- OrderID: "sub2_mobile_wap",
- Amount: "18.00",
- Subject: "Balance recharge",
- IsMobile: true,
- })
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if precreateCalls != 0 || wapPayCalls != 1 {
- t.Fatalf("precreate calls = %d, wap calls = %d; want 0, 1", precreateCalls, wapPayCalls)
- }
- if resp.PayURL == "" || resp.QRCode != "" {
- t.Fatalf("unexpected response: qr_code=%q pay_url=%q", resp.QRCode, resp.PayURL)
- }
- }
- func TestCreateTradeUsesPrecreateForDesktopWhenAvailable(t *testing.T) {
- origPreCreate := alipayTradePreCreate
- origPagePay := alipayTradePagePay
- t.Cleanup(func() {
- alipayTradePreCreate = origPreCreate
- alipayTradePagePay = origPagePay
- })
- preCreateCalls := 0
- pagePayCalls := 0
- alipayTradePreCreate = func(ctx context.Context, client *alipay.Client, param alipay.TradePreCreate) (*alipay.TradePreCreateRsp, error) {
- preCreateCalls++
- if param.ProductCode != alipayProductCodePreCreate {
- t.Fatalf("product_code = %q, want %q", param.ProductCode, alipayProductCodePreCreate)
- }
- return &alipay.TradePreCreateRsp{
- Error: alipay.Error{Code: alipay.CodeSuccess},
- QRCode: "https://qr.alipay.example.com/precreate-token",
- }, nil
- }
- alipayTradePagePay = func(client *alipay.Client, param alipay.TradePagePay) (*url.URL, error) {
- pagePayCalls++
- return url.Parse("https://openapi.alipay.com/gateway.do?page-pay")
- }
- provider := &Alipay{}
- resp, err := provider.createDesktopTrade(context.Background(), &alipay.Client{}, payment.CreatePaymentRequest{
- OrderID: "sub2_102",
- Amount: "66.00",
- Subject: "Balance recharge",
- }, "https://merchant.example.com/api/v1/payment/webhook/alipay", "https://merchant.example.com/payment/result")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if preCreateCalls != 1 {
- t.Fatalf("precreate calls = %d, want 1", preCreateCalls)
- }
- if pagePayCalls != 0 {
- t.Fatalf("page pay calls = %d, want 0", pagePayCalls)
- }
- if resp.QRCode != "https://qr.alipay.example.com/precreate-token" {
- t.Fatalf("qr_code = %q", resp.QRCode)
- }
- if resp.PayURL != "" {
- t.Fatalf("pay_url = %q, want empty for precreate", resp.PayURL)
- }
- }
- func TestAlipayMerchantIdentityMetadata(t *testing.T) {
- t.Parallel()
- provider := &Alipay{
- config: map[string]string{
- "appId": "2021001234567890",
- },
- }
- metadata := provider.MerchantIdentityMetadata()
- if metadata["app_id"] != "2021001234567890" {
- t.Fatalf("app_id = %q, want %q", metadata["app_id"], "2021001234567890")
- }
- }
- func TestParseAlipayAmount(t *testing.T) {
- t.Parallel()
- amount, err := parseAlipayAmount("", "88.00", "77.00")
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if amount != 88 {
- t.Fatalf("amount = %v, want 88", amount)
- }
- if _, err := parseAlipayAmount("", "not-a-number"); err == nil {
- t.Fatal("expected error when no valid amount field exists")
- }
- }
|