//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") } if resp.QRCode != resp.PayURL { t.Fatalf("qr_code = %q, want same as pay_url %q", resp.QRCode, resp.PayURL) } } 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 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") } }