| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824 |
- //go:build unit
- package service
- import (
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/base64"
- "encoding/json"
- "net/url"
- "strconv"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- )
- func TestNormalizeVisibleMethods(t *testing.T) {
- t.Parallel()
- got := NormalizeVisibleMethods([]string{
- "alipay_direct",
- "alipay",
- " wxpay_direct ",
- "wxpay",
- "stripe",
- "ldc",
- })
- want := []string{"alipay", "wxpay", "stripe", "ldc"}
- if len(got) != len(want) {
- t.Fatalf("NormalizeVisibleMethods len = %d, want %d (%v)", len(got), len(want), got)
- }
- for i := range want {
- if got[i] != want[i] {
- t.Fatalf("NormalizeVisibleMethods[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
- }
- }
- }
- func TestEnabledVisibleMethodsForEasyPayIncludesCustomSupportedTypes(t *testing.T) {
- t.Parallel()
- got := enabledVisibleMethodsForProvider(payment.TypeEasyPay, "alipay,ldc,usdt_trc20")
- want := []string{"alipay", "ldc", "usdt_trc20"}
- if len(got) != len(want) {
- t.Fatalf("enabledVisibleMethodsForProvider len = %d, want %d (%v)", len(got), len(want), got)
- }
- for i := range want {
- if got[i] != want[i] {
- t.Fatalf("enabledVisibleMethodsForProvider[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
- }
- }
- }
- func TestNormalizePaymentSource(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- input string
- expect string
- }{
- {name: "empty uses default", input: "", expect: PaymentSourceHostedRedirect},
- {name: "wechat alias normalized", input: "wechat_in_app", expect: PaymentSourceWechatInAppResume},
- {name: "canonical value preserved", input: PaymentSourceWechatInAppResume, expect: PaymentSourceWechatInAppResume},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- if got := NormalizePaymentSource(tt.input); got != tt.expect {
- t.Fatalf("NormalizePaymentSource(%q) = %q, want %q", tt.input, got, tt.expect)
- }
- })
- }
- }
- func TestCanonicalizeReturnURL(t *testing.T) {
- t.Parallel()
- got, err := CanonicalizeReturnURL("https://example.com/payment/result?b=2#a", "example.com", "")
- if err != nil {
- t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
- }
- if got != "https://example.com/payment/result?b=2" {
- t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://example.com/payment/result?b=2")
- }
- }
- func TestCanonicalizeReturnURLRejectsRelativeURL(t *testing.T) {
- t.Parallel()
- if _, err := CanonicalizeReturnURL("/payment/result", "example.com", ""); err == nil {
- t.Fatal("CanonicalizeReturnURL should reject relative URLs")
- }
- }
- func TestCanonicalizeReturnURLRejectsExternalHost(t *testing.T) {
- t.Parallel()
- if _, err := CanonicalizeReturnURL("https://evil.example/payment/result", "app.example.com", ""); err == nil {
- t.Fatal("CanonicalizeReturnURL should reject external hosts")
- }
- }
- func TestCanonicalizeReturnURLAllowsConfiguredFrontendHost(t *testing.T) {
- t.Parallel()
- got, err := CanonicalizeReturnURL(
- "https://app.example.com/payment/result?from=checkout",
- "api.example.com",
- "https://app.example.com/purchase",
- )
- if err != nil {
- t.Fatalf("CanonicalizeReturnURL returned error: %v", err)
- }
- if got != "https://app.example.com/payment/result?from=checkout" {
- t.Fatalf("CanonicalizeReturnURL = %q, want %q", got, "https://app.example.com/payment/result?from=checkout")
- }
- }
- func TestCanonicalizeReturnURLRejectsNonCanonicalPath(t *testing.T) {
- t.Parallel()
- if _, err := CanonicalizeReturnURL("https://app.example.com/orders/42", "app.example.com", ""); err == nil {
- t.Fatal("CanonicalizeReturnURL should reject non-canonical result paths")
- }
- }
- func TestBuildPaymentReturnURL(t *testing.T) {
- t.Parallel()
- got, err := buildPaymentReturnURL("https://example.com/payment/result?from=checkout#fragment", 42, "sub2_42", "resume-token")
- if err != nil {
- t.Fatalf("buildPaymentReturnURL returned error: %v", err)
- }
- parsed, err := url.Parse(got)
- if err != nil {
- t.Fatalf("url.Parse returned error: %v", err)
- }
- if parsed.Fragment != "" {
- t.Fatalf("buildPaymentReturnURL should strip fragments, got %q", parsed.Fragment)
- }
- query := parsed.Query()
- if query.Get("from") != "checkout" {
- t.Fatalf("expected original query to be preserved, got %q", query.Get("from"))
- }
- if query.Get("order_id") != strconv.FormatInt(42, 10) {
- t.Fatalf("order_id = %q", query.Get("order_id"))
- }
- if query.Get("out_trade_no") != "sub2_42" {
- t.Fatalf("out_trade_no = %q", query.Get("out_trade_no"))
- }
- if query.Get("resume_token") != "resume-token" {
- t.Fatalf("resume_token = %q", query.Get("resume_token"))
- }
- if query.Get("status") != "success" {
- t.Fatalf("status = %q", query.Get("status"))
- }
- }
- func TestBuildPaymentReturnURLWithoutResumeTokenStillIncludesOutTradeNo(t *testing.T) {
- t.Parallel()
- got, err := buildPaymentReturnURL("https://example.com/payment/result", 42, "sub2_42", "")
- if err != nil {
- t.Fatalf("buildPaymentReturnURL returned error: %v", err)
- }
- parsed, err := url.Parse(got)
- if err != nil {
- t.Fatalf("url.Parse returned error: %v", err)
- }
- query := parsed.Query()
- if query.Get("order_id") != "42" {
- t.Fatalf("order_id = %q", query.Get("order_id"))
- }
- if query.Get("out_trade_no") != "sub2_42" {
- t.Fatalf("out_trade_no = %q", query.Get("out_trade_no"))
- }
- if query.Get("resume_token") != "" {
- t.Fatalf("resume_token = %q, want empty", query.Get("resume_token"))
- }
- }
- func TestBuildPaymentReturnURLEmptyBase(t *testing.T) {
- t.Parallel()
- got, err := buildPaymentReturnURL("", 42, "sub2_42", "resume-token")
- if err != nil {
- t.Fatalf("buildPaymentReturnURL returned error: %v", err)
- }
- if got != "" {
- t.Fatalf("buildPaymentReturnURL = %q, want empty string", got)
- }
- }
- func TestPaymentResumeTokenRoundTrip(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
- token, err := svc.CreateToken(ResumeTokenClaims{
- OrderID: 42,
- UserID: 7,
- ProviderInstanceID: "19",
- ProviderKey: "easypay",
- PaymentType: "wxpay",
- CanonicalReturnURL: "https://example.com/payment/result",
- IssuedAt: 1234567890,
- })
- if err != nil {
- t.Fatalf("CreateToken returned error: %v", err)
- }
- claims, err := svc.ParseToken(token)
- if err != nil {
- t.Fatalf("ParseToken returned error: %v", err)
- }
- if claims.OrderID != 42 || claims.UserID != 7 {
- t.Fatalf("claims mismatch: %+v", claims)
- }
- if claims.ProviderInstanceID != "19" || claims.ProviderKey != "easypay" || claims.PaymentType != "wxpay" {
- t.Fatalf("claims provider snapshot mismatch: %+v", claims)
- }
- if claims.CanonicalReturnURL != "https://example.com/payment/result" {
- t.Fatalf("claims return URL = %q", claims.CanonicalReturnURL)
- }
- }
- func TestCreateTokenRejectsMissingSigningKey(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService(nil)
- _, err := svc.CreateToken(ResumeTokenClaims{OrderID: 42})
- if err == nil {
- t.Fatal("CreateToken should reject missing signing key")
- }
- }
- func TestParseTokenRejectsFallbackSignedTokenWhenSigningKeyMissing(t *testing.T) {
- t.Parallel()
- token := mustCreateFallbackSignedToken(t, ResumeTokenClaims{OrderID: 42, UserID: 7})
- svc := NewPaymentResumeService(nil)
- _, err := svc.ParseToken(token)
- if err == nil {
- t.Fatal("ParseToken should reject tokens when signing key is missing")
- }
- }
- func TestParseTokenRejectsExpiredToken(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
- token, err := svc.CreateToken(ResumeTokenClaims{
- OrderID: 42,
- UserID: 7,
- IssuedAt: time.Now().Add(-25 * time.Hour).Unix(),
- ExpiresAt: time.Now().Add(-1 * time.Hour).Unix(),
- })
- if err != nil {
- t.Fatalf("CreateToken returned error: %v", err)
- }
- _, err = svc.ParseToken(token)
- if err == nil {
- t.Fatal("ParseToken should reject expired tokens")
- }
- }
- func TestWeChatPaymentResumeTokenRoundTrip(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
- token, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-123",
- PaymentType: payment.TypeWxpay,
- Amount: "12.50",
- OrderType: payment.OrderTypeSubscription,
- PlanID: 7,
- RedirectTo: "/purchase?from=wechat",
- Scope: "snsapi_base",
- IssuedAt: 1234567890,
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- claims, err := svc.ParseWeChatPaymentResumeToken(token)
- if err != nil {
- t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
- }
- if claims.OpenID != "openid-123" || claims.PaymentType != payment.TypeWxpay {
- t.Fatalf("claims mismatch: %+v", claims)
- }
- if claims.Amount != "12.50" || claims.OrderType != payment.OrderTypeSubscription || claims.PlanID != 7 {
- t.Fatalf("claims payment context mismatch: %+v", claims)
- }
- if claims.RedirectTo != "/purchase?from=wechat" || claims.Scope != "snsapi_base" {
- t.Fatalf("claims redirect/scope mismatch: %+v", claims)
- }
- }
- func TestCreateWeChatPaymentResumeTokenRejectsMissingSigningKey(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService(nil)
- _, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{OpenID: "openid-123"})
- if err == nil {
- t.Fatal("CreateWeChatPaymentResumeToken should reject missing signing key")
- }
- }
- func TestParseWeChatPaymentResumeTokenRejectsFallbackSignedTokenWhenSigningKeyMissing(t *testing.T) {
- t.Parallel()
- token := mustCreateFallbackSignedToken(t, WeChatPaymentResumeClaims{
- TokenType: wechatPaymentResumeTokenType,
- OpenID: "openid-123",
- PaymentType: payment.TypeWxpay,
- })
- svc := NewPaymentResumeService(nil)
- _, err := svc.ParseWeChatPaymentResumeToken(token)
- if err == nil {
- t.Fatal("ParseWeChatPaymentResumeToken should reject tokens when signing key is missing")
- }
- }
- func TestParseWeChatPaymentResumeTokenRejectsExpiredToken(t *testing.T) {
- t.Parallel()
- svc := NewPaymentResumeService([]byte("0123456789abcdef0123456789abcdef"))
- token, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-123",
- PaymentType: payment.TypeWxpay,
- IssuedAt: time.Now().Add(-30 * time.Minute).Unix(),
- ExpiresAt: time.Now().Add(-1 * time.Minute).Unix(),
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- _, err = svc.ParseWeChatPaymentResumeToken(token)
- if err == nil {
- t.Fatal("ParseWeChatPaymentResumeToken should reject expired tokens")
- }
- }
- func TestPaymentServiceParseWeChatPaymentResumeTokenUsesExplicitSigningKey(t *testing.T) {
- t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
- token, err := NewPaymentResumeService([]byte("explicit-payment-resume-signing-key")).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-explicit-key",
- PaymentType: payment.TypeWxpay,
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- svc := &PaymentService{
- configService: &PaymentConfigService{
- encryptionKey: []byte("0123456789abcdef0123456789abcdef"),
- },
- }
- claims, err := svc.ParseWeChatPaymentResumeToken(token)
- if err != nil {
- t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
- }
- if claims.OpenID != "openid-explicit-key" {
- t.Fatalf("openid = %q, want %q", claims.OpenID, "openid-explicit-key")
- }
- }
- func TestPaymentServiceParseWeChatPaymentResumeTokenAcceptsLegacyEncryptionKeyDuringMigration(t *testing.T) {
- t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
- legacyKey := []byte("0123456789abcdef0123456789abcdef")
- token, err := NewPaymentResumeService(legacyKey).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-legacy-key",
- PaymentType: payment.TypeWxpay,
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- svc := &PaymentService{
- configService: &PaymentConfigService{
- encryptionKey: legacyKey,
- },
- }
- claims, err := svc.ParseWeChatPaymentResumeToken(token)
- if err != nil {
- t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
- }
- if claims.OpenID != "openid-legacy-key" {
- t.Fatalf("openid = %q, want %q", claims.OpenID, "openid-legacy-key")
- }
- }
- func TestNewConfiguredPaymentResumeServicePrefersExplicitSigningKeyAndKeepsLegacyVerificationFallback(t *testing.T) {
- t.Setenv("PAYMENT_RESUME_SIGNING_KEY", "explicit-payment-resume-signing-key")
- legacyKey := []byte("0123456789abcdef0123456789abcdef")
- svc := newLegacyAwarePaymentResumeService(legacyKey)
- explicitToken, err := svc.CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-explicit-key",
- PaymentType: payment.TypeWxpay,
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- explicitClaims, err := NewPaymentResumeService([]byte("explicit-payment-resume-signing-key")).ParseWeChatPaymentResumeToken(explicitToken)
- if err != nil {
- t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
- }
- if explicitClaims.OpenID != "openid-explicit-key" {
- t.Fatalf("openid = %q, want %q", explicitClaims.OpenID, "openid-explicit-key")
- }
- legacyToken, err := NewPaymentResumeService(legacyKey).CreateWeChatPaymentResumeToken(WeChatPaymentResumeClaims{
- OpenID: "openid-legacy-key",
- PaymentType: payment.TypeWxpay,
- })
- if err != nil {
- t.Fatalf("CreateWeChatPaymentResumeToken returned error: %v", err)
- }
- legacyClaims, err := svc.ParseWeChatPaymentResumeToken(legacyToken)
- if err != nil {
- t.Fatalf("ParseWeChatPaymentResumeToken returned error: %v", err)
- }
- if legacyClaims.OpenID != "openid-legacy-key" {
- t.Fatalf("openid = %q, want %q", legacyClaims.OpenID, "openid-legacy-key")
- }
- }
- func TestNormalizeVisibleMethodSource(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- method string
- input string
- want string
- }{
- {name: "alipay official alias", method: payment.TypeAlipay, input: "alipay", want: VisibleMethodSourceOfficialAlipay},
- {name: "alipay easypay alias", method: payment.TypeAlipay, input: "easypay", want: VisibleMethodSourceEasyPayAlipay},
- {name: "wxpay official alias", method: payment.TypeWxpay, input: "wxpay", want: VisibleMethodSourceOfficialWechat},
- {name: "wxpay easypay alias", method: payment.TypeWxpay, input: "easypay", want: VisibleMethodSourceEasyPayWechat},
- {name: "unsupported source", method: payment.TypeWxpay, input: "stripe", want: ""},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- if got := NormalizeVisibleMethodSource(tt.method, tt.input); got != tt.want {
- t.Fatalf("NormalizeVisibleMethodSource(%q, %q) = %q, want %q", tt.method, tt.input, got, tt.want)
- }
- })
- }
- }
- func TestVisibleMethodProviderKeyForSource(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- method string
- source string
- want string
- ok bool
- }{
- {name: "official alipay", method: payment.TypeAlipay, source: VisibleMethodSourceOfficialAlipay, want: payment.TypeAlipay, ok: true},
- {name: "easypay alipay", method: payment.TypeAlipay, source: VisibleMethodSourceEasyPayAlipay, want: payment.TypeEasyPay, ok: true},
- {name: "official wechat", method: payment.TypeWxpay, source: VisibleMethodSourceOfficialWechat, want: payment.TypeWxpay, ok: true},
- {name: "easypay wechat", method: payment.TypeWxpay, source: VisibleMethodSourceEasyPayWechat, want: payment.TypeEasyPay, ok: true},
- {name: "mismatched method and source", method: payment.TypeAlipay, source: VisibleMethodSourceOfficialWechat, want: "", ok: false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got, ok := VisibleMethodProviderKeyForSource(tt.method, tt.source)
- if got != tt.want || ok != tt.ok {
- t.Fatalf("VisibleMethodProviderKeyForSource(%q, %q) = (%q, %v), want (%q, %v)", tt.method, tt.source, got, ok, tt.want, tt.ok)
- }
- })
- }
- }
- func TestVisibleMethodLoadBalancerUsesEnabledProviderInstance(t *testing.T) {
- t.Parallel()
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("Official Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetSortOrder(1).
- Save(ctx)
- if err != nil {
- t.Fatalf("create alipay provider: %v", err)
- }
- inner := &captureLoadBalancer{}
- configService := &PaymentConfigService{
- entClient: client,
- }
- lb := newVisibleMethodLoadBalancer(inner, configService)
- _, err = lb.SelectInstance(ctx, "", payment.TypeAlipay, payment.StrategyRoundRobin, 12.5)
- if err != nil {
- t.Fatalf("SelectInstance returned error: %v", err)
- }
- if inner.lastProviderKey != payment.TypeAlipay {
- t.Fatalf("lastProviderKey = %q, want %q", inner.lastProviderKey, payment.TypeAlipay)
- }
- }
- func TestVisibleMethodLoadBalancerUsesConfiguredSourceWhenMultipleProvidersEnabled(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- method payment.PaymentType
- officialName string
- officialTypes string
- easyPayName string
- easyPayTypes string
- sourceSetting string
- wantProvider string
- }{
- {
- name: "alipay uses official source",
- method: payment.TypeAlipay,
- officialName: "Official Alipay",
- officialTypes: "alipay",
- easyPayName: "EasyPay Alipay",
- easyPayTypes: "alipay",
- sourceSetting: VisibleMethodSourceOfficialAlipay,
- wantProvider: payment.TypeAlipay,
- },
- {
- name: "alipay uses easypay source",
- method: payment.TypeAlipay,
- officialName: "Official Alipay",
- officialTypes: "alipay",
- easyPayName: "EasyPay Alipay",
- easyPayTypes: "alipay",
- sourceSetting: VisibleMethodSourceEasyPayAlipay,
- wantProvider: payment.TypeEasyPay,
- },
- {
- name: "wxpay uses official source",
- method: payment.TypeWxpay,
- officialName: "Official WeChat",
- officialTypes: "wxpay",
- easyPayName: "EasyPay WeChat",
- easyPayTypes: "wxpay",
- sourceSetting: VisibleMethodSourceOfficialWechat,
- wantProvider: payment.TypeWxpay,
- },
- {
- name: "wxpay uses easypay source",
- method: payment.TypeWxpay,
- officialName: "Official WeChat",
- officialTypes: "wxpay",
- easyPayName: "EasyPay WeChat",
- easyPayTypes: "wxpay",
- sourceSetting: VisibleMethodSourceEasyPayWechat,
- wantProvider: payment.TypeEasyPay,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- officialProviderKey := payment.TypeAlipay
- if tt.method == payment.TypeWxpay {
- officialProviderKey = payment.TypeWxpay
- }
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(officialProviderKey).
- SetName(tt.officialName).
- SetConfig("{}").
- SetSupportedTypes(tt.officialTypes).
- SetEnabled(true).
- SetSortOrder(1).
- Save(ctx)
- if err != nil {
- t.Fatalf("create official provider: %v", err)
- }
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName(tt.easyPayName).
- SetConfig("{}").
- SetSupportedTypes(tt.easyPayTypes).
- SetEnabled(true).
- SetSortOrder(2).
- Save(ctx)
- if err != nil {
- t.Fatalf("create easypay provider: %v", err)
- }
- inner := &captureLoadBalancer{}
- configService := &PaymentConfigService{
- entClient: client,
- settingRepo: &paymentConfigSettingRepoStub{
- values: map[string]string{
- visibleMethodSourceSettingKey(tt.method): tt.sourceSetting,
- },
- },
- }
- lb := newVisibleMethodLoadBalancer(inner, configService)
- _, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 12.5)
- if err != nil {
- t.Fatalf("SelectInstance returned error: %v", err)
- }
- if inner.lastProviderKey != tt.wantProvider {
- t.Fatalf("lastProviderKey = %q, want %q", inner.lastProviderKey, tt.wantProvider)
- }
- })
- }
- }
- func TestVisibleMethodLoadBalancerPreservesLegacyCrossProviderRoutingWhenSourceMissing(t *testing.T) {
- t.Parallel()
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeAlipay).
- SetName("Official Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetSortOrder(1).
- Save(ctx)
- if err != nil {
- t.Fatalf("create official provider: %v", err)
- }
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName("EasyPay Alipay").
- SetConfig("{}").
- SetSupportedTypes("alipay").
- SetEnabled(true).
- SetSortOrder(2).
- Save(ctx)
- if err != nil {
- t.Fatalf("create easypay provider: %v", err)
- }
- inner := &captureLoadBalancer{}
- configService := &PaymentConfigService{
- entClient: client,
- settingRepo: &paymentConfigSettingRepoStub{
- values: map[string]string{
- visibleMethodSourceSettingKey(payment.TypeAlipay): "",
- },
- },
- }
- lb := newVisibleMethodLoadBalancer(inner, configService)
- _, err = lb.SelectInstance(ctx, "", payment.TypeAlipay, payment.StrategyRoundRobin, 9.9)
- if err != nil {
- t.Fatalf("SelectInstance returned error: %v", err)
- }
- if inner.lastProviderKey != "" {
- t.Fatalf("lastProviderKey = %q, want legacy cross-provider empty key", inner.lastProviderKey)
- }
- if inner.lastPaymentType != payment.TypeAlipay {
- t.Fatalf("lastPaymentType = %q, want %q", inner.lastPaymentType, payment.TypeAlipay)
- }
- }
- func TestVisibleMethodLoadBalancerRejectsInvalidSourceWhenMultipleProvidersEnabled(t *testing.T) {
- t.Parallel()
- tests := []struct {
- name string
- method payment.PaymentType
- sourceValue string
- wantMessage string
- }{
- {
- name: "invalid wxpay source",
- method: payment.TypeWxpay,
- sourceValue: "stripe",
- wantMessage: "wxpay source must be one of the supported payment providers",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- ctx := context.Background()
- client := newPaymentConfigServiceTestClient(t)
- officialProviderKey := payment.TypeAlipay
- officialSupportedTypes := "alipay"
- officialName := "Official Alipay"
- easyPaySupportedTypes := "alipay"
- easyPayName := "EasyPay Alipay"
- if tt.method == payment.TypeWxpay {
- officialProviderKey = payment.TypeWxpay
- officialSupportedTypes = "wxpay"
- officialName = "Official WeChat"
- easyPaySupportedTypes = "wxpay"
- easyPayName = "EasyPay WeChat"
- }
- _, err := client.PaymentProviderInstance.Create().
- SetProviderKey(officialProviderKey).
- SetName(officialName).
- SetConfig("{}").
- SetSupportedTypes(officialSupportedTypes).
- SetEnabled(true).
- SetSortOrder(1).
- Save(ctx)
- if err != nil {
- t.Fatalf("create official provider: %v", err)
- }
- _, err = client.PaymentProviderInstance.Create().
- SetProviderKey(payment.TypeEasyPay).
- SetName(easyPayName).
- SetConfig("{}").
- SetSupportedTypes(easyPaySupportedTypes).
- SetEnabled(true).
- SetSortOrder(2).
- Save(ctx)
- if err != nil {
- t.Fatalf("create easypay provider: %v", err)
- }
- inner := &captureLoadBalancer{}
- configService := &PaymentConfigService{
- entClient: client,
- settingRepo: &paymentConfigSettingRepoStub{
- values: map[string]string{
- visibleMethodSourceSettingKey(tt.method): tt.sourceValue,
- },
- },
- }
- lb := newVisibleMethodLoadBalancer(inner, configService)
- _, err = lb.SelectInstance(ctx, "", tt.method, payment.StrategyRoundRobin, 9.9)
- if err == nil {
- t.Fatal("SelectInstance should reject invalid visible method source configuration")
- }
- if infraerrors.Reason(err) != "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE" {
- t.Fatalf("Reason(err) = %q, want %q", infraerrors.Reason(err), "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE")
- }
- if infraerrors.Message(err) != tt.wantMessage {
- t.Fatalf("Message(err) = %q, want %q", infraerrors.Message(err), tt.wantMessage)
- }
- })
- }
- }
- func TestVisibleMethodLoadBalancerRejectsMissingEnabledVisibleMethodProvider(t *testing.T) {
- t.Parallel()
- inner := &captureLoadBalancer{}
- configService := &PaymentConfigService{
- entClient: newPaymentConfigServiceTestClient(t),
- }
- lb := newVisibleMethodLoadBalancer(inner, configService)
- if _, err := lb.SelectInstance(context.Background(), "", payment.TypeWxpay, payment.StrategyRoundRobin, 9.9); err == nil {
- t.Fatal("SelectInstance should reject when no enabled provider instance exists")
- }
- }
- type captureLoadBalancer struct {
- lastProviderKey string
- lastPaymentType string
- }
- func (c *captureLoadBalancer) GetInstanceConfig(context.Context, int64) (map[string]string, error) {
- return map[string]string{}, nil
- }
- func (c *captureLoadBalancer) SelectInstance(_ context.Context, providerKey string, paymentType payment.PaymentType, _ payment.Strategy, _ float64) (*payment.InstanceSelection, error) {
- c.lastProviderKey = providerKey
- c.lastPaymentType = paymentType
- return &payment.InstanceSelection{ProviderKey: providerKey, SupportedTypes: paymentType}, nil
- }
- func mustCreateFallbackSignedToken(t *testing.T, claims any) string {
- t.Helper()
- payload, err := json.Marshal(claims)
- if err != nil {
- t.Fatalf("marshal claims: %v", err)
- }
- encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
- mac := hmac.New(sha256.New, []byte("sub2api-payment-resume"))
- _, _ = mac.Write([]byte(encodedPayload))
- signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
- return encodedPayload + "." + signature
- }
|