diff --git a/backend/internal/payment/crypto_test.go b/backend/internal/payment/crypto_test.go new file mode 100644 index 0000000000..da8b6006e1 --- /dev/null +++ b/backend/internal/payment/crypto_test.go @@ -0,0 +1,183 @@ +package payment + +import ( + "crypto/rand" + "strings" + "testing" +) + +func makeKey(t *testing.T) []byte { + t.Helper() + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + t.Fatalf("generate random key: %v", err) + } + return key +} + +func TestEncryptDecryptRoundTrip(t *testing.T) { + t.Parallel() + key := makeKey(t) + + plaintexts := []string{ + "hello world", + "short", + "a longer string with special chars: !@#$%^&*()", + `{"key":"value","num":42}`, + "你好世界 unicode test 🎉", + strings.Repeat("x", 10000), + } + + for _, pt := range plaintexts { + encrypted, err := Encrypt(pt, key) + if err != nil { + t.Fatalf("Encrypt(%q) error: %v", pt[:min(len(pt), 30)], err) + } + decrypted, err := Decrypt(encrypted, key) + if err != nil { + t.Fatalf("Decrypt error for plaintext %q: %v", pt[:min(len(pt), 30)], err) + } + if decrypted != pt { + t.Fatalf("round-trip failed: got %q, want %q", decrypted[:min(len(decrypted), 30)], pt[:min(len(pt), 30)]) + } + } +} + +func TestEncryptProducesDifferentCiphertexts(t *testing.T) { + t.Parallel() + key := makeKey(t) + + ct1, err := Encrypt("same plaintext", key) + if err != nil { + t.Fatalf("first Encrypt error: %v", err) + } + ct2, err := Encrypt("same plaintext", key) + if err != nil { + t.Fatalf("second Encrypt error: %v", err) + } + if ct1 == ct2 { + t.Fatal("two encryptions of the same plaintext should produce different ciphertexts (random nonce)") + } +} + +func TestDecryptWithWrongKeyFails(t *testing.T) { + t.Parallel() + key1 := makeKey(t) + key2 := makeKey(t) + + encrypted, err := Encrypt("secret data", key1) + if err != nil { + t.Fatalf("Encrypt error: %v", err) + } + + _, err = Decrypt(encrypted, key2) + if err == nil { + t.Fatal("Decrypt with wrong key should fail, but got nil error") + } +} + +func TestEncryptRejectsInvalidKeyLength(t *testing.T) { + t.Parallel() + badKeys := [][]byte{ + nil, + make([]byte, 0), + make([]byte, 16), + make([]byte, 31), + make([]byte, 33), + make([]byte, 64), + } + for _, key := range badKeys { + _, err := Encrypt("test", key) + if err == nil { + t.Fatalf("Encrypt should reject key of length %d", len(key)) + } + } +} + +func TestDecryptRejectsInvalidKeyLength(t *testing.T) { + t.Parallel() + badKeys := [][]byte{ + nil, + make([]byte, 16), + make([]byte, 33), + } + for _, key := range badKeys { + _, err := Decrypt("dummydata:dummydata:dummydata", key) + if err == nil { + t.Fatalf("Decrypt should reject key of length %d", len(key)) + } + } +} + +func TestEncryptEmptyPlaintext(t *testing.T) { + t.Parallel() + key := makeKey(t) + + encrypted, err := Encrypt("", key) + if err != nil { + t.Fatalf("Encrypt empty plaintext error: %v", err) + } + decrypted, err := Decrypt(encrypted, key) + if err != nil { + t.Fatalf("Decrypt empty plaintext error: %v", err) + } + if decrypted != "" { + t.Fatalf("expected empty string, got %q", decrypted) + } +} + +func TestEncryptDecryptUnicodeJSON(t *testing.T) { + t.Parallel() + key := makeKey(t) + + jsonContent := `{"name":"测试用户","email":"test@example.com","balance":100.50}` + encrypted, err := Encrypt(jsonContent, key) + if err != nil { + t.Fatalf("Encrypt JSON error: %v", err) + } + decrypted, err := Decrypt(encrypted, key) + if err != nil { + t.Fatalf("Decrypt JSON error: %v", err) + } + if decrypted != jsonContent { + t.Fatalf("JSON round-trip failed: got %q, want %q", decrypted, jsonContent) + } +} + +func TestDecryptInvalidFormat(t *testing.T) { + t.Parallel() + key := makeKey(t) + + invalidInputs := []string{ + "", + "nodelimiter", + "only:two", + "invalid:base64:!!!", + } + for _, input := range invalidInputs { + _, err := Decrypt(input, key) + if err == nil { + t.Fatalf("Decrypt(%q) should fail but got nil error", input) + } + } +} + +func TestCiphertextFormat(t *testing.T) { + t.Parallel() + key := makeKey(t) + + encrypted, err := Encrypt("test", key) + if err != nil { + t.Fatalf("Encrypt error: %v", err) + } + + parts := strings.SplitN(encrypted, ":", 3) + if len(parts) != 3 { + t.Fatalf("ciphertext should have format iv:authTag:ciphertext, got %d parts", len(parts)) + } + for i, part := range parts { + if part == "" { + t.Fatalf("ciphertext part %d is empty", i) + } + } +} diff --git a/backend/internal/payment/fee_test.go b/backend/internal/payment/fee_test.go new file mode 100644 index 0000000000..c58d1082a9 --- /dev/null +++ b/backend/internal/payment/fee_test.go @@ -0,0 +1,111 @@ +package payment + +import ( + "testing" +) + +func TestCalculatePayAmount(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + amount float64 + feeRate float64 + expected string + }{ + { + name: "zero fee rate returns same amount", + amount: 100.00, + feeRate: 0, + expected: "100.00", + }, + { + name: "negative fee rate returns same amount", + amount: 50.00, + feeRate: -5, + expected: "50.00", + }, + { + name: "1 percent fee rate", + amount: 100.00, + feeRate: 1, + expected: "101.00", + }, + { + name: "5 percent fee on 200", + amount: 200.00, + feeRate: 5, + expected: "210.00", + }, + { + name: "fee rounds UP to 2 decimal places", + amount: 100.00, + feeRate: 3, + expected: "103.00", + }, + { + name: "fee rounds UP small remainder", + amount: 10.00, + feeRate: 3.33, + expected: "10.34", // 10 * 3.33 / 100 = 0.333 -> round up -> 0.34 + }, + { + name: "very small amount", + amount: 0.01, + feeRate: 1, + expected: "0.02", // 0.01 * 1/100 = 0.0001 -> round up -> 0.01 -> total 0.02 + }, + { + name: "large amount", + amount: 99999.99, + feeRate: 10, + expected: "109999.99", // 99999.99 * 10/100 = 9999.999 -> round up -> 10000.00 -> total 109999.99 + }, + { + name: "100 percent fee rate doubles amount", + amount: 50.00, + feeRate: 100, + expected: "100.00", + }, + { + name: "precision 0.01 fee difference", + amount: 100.00, + feeRate: 1.01, + expected: "101.01", // 100 * 1.01/100 = 1.01 + }, + { + name: "precision 0.02 fee", + amount: 100.00, + feeRate: 1.02, + expected: "101.02", + }, + { + name: "zero amount with positive fee", + amount: 0, + feeRate: 5, + expected: "0.00", + }, + { + name: "fractional amount no fee", + amount: 19.99, + feeRate: 0, + expected: "19.99", + }, + { + name: "fractional fee that causes rounding up", + amount: 33.33, + feeRate: 7.77, + expected: "35.92", // 33.33 * 7.77 / 100 = 2.589741 -> round up -> 2.59 -> total 35.92 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := CalculatePayAmount(tt.amount, tt.feeRate) + if got != tt.expected { + t.Fatalf("CalculatePayAmount(%v, %v) = %q, want %q", tt.amount, tt.feeRate, got, tt.expected) + } + }) + } +} diff --git a/backend/internal/payment/load_balancer_test.go b/backend/internal/payment/load_balancer_test.go new file mode 100644 index 0000000000..78316cc4e6 --- /dev/null +++ b/backend/internal/payment/load_balancer_test.go @@ -0,0 +1,87 @@ +package payment + +import ( + "testing" +) + +func TestContainsType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + supportedTypes string + target PaymentType + expected bool + }{ + { + name: "exact match single type", + supportedTypes: "alipay", + target: "alipay", + expected: true, + }, + { + name: "no match single type", + supportedTypes: "wxpay", + target: "alipay", + expected: false, + }, + { + name: "match in comma-separated list", + supportedTypes: "alipay,wxpay,stripe", + target: "wxpay", + expected: true, + }, + { + name: "first in comma-separated list", + supportedTypes: "alipay,wxpay", + target: "alipay", + expected: true, + }, + { + name: "last in comma-separated list", + supportedTypes: "alipay,wxpay,stripe", + target: "stripe", + expected: true, + }, + { + name: "no match in comma-separated list", + supportedTypes: "alipay,wxpay", + target: "stripe", + expected: false, + }, + { + name: "empty target", + supportedTypes: "alipay,wxpay", + target: "", + expected: false, + }, + { + name: "types with spaces are trimmed", + supportedTypes: " alipay , wxpay ", + target: "alipay", + expected: true, + }, + { + name: "partial match should not succeed", + supportedTypes: "alipay_direct", + target: "alipay", + expected: false, + }, + { + name: "empty supported types string matches nothing", + supportedTypes: "", + target: "alipay", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := containsType(tt.supportedTypes, tt.target) + if got != tt.expected { + t.Fatalf("containsType(%q, %q) = %v, want %v", tt.supportedTypes, tt.target, got, tt.expected) + } + }) + } +} diff --git a/backend/internal/payment/provider/easypay_sign_test.go b/backend/internal/payment/provider/easypay_sign_test.go new file mode 100644 index 0000000000..146a6fa1af --- /dev/null +++ b/backend/internal/payment/provider/easypay_sign_test.go @@ -0,0 +1,180 @@ +package provider + +import ( + "testing" +) + +func TestEasyPaySignConsistentOutput(t *testing.T) { + t.Parallel() + + params := map[string]string{ + "pid": "1001", + "type": "alipay", + "out_trade_no": "ORDER123", + "name": "Test Product", + "money": "10.00", + } + pkey := "test_secret_key" + + sign1 := easyPaySign(params, pkey) + sign2 := easyPaySign(params, pkey) + if sign1 != sign2 { + t.Fatalf("easyPaySign should be deterministic: %q != %q", sign1, sign2) + } + if len(sign1) != 32 { + t.Fatalf("MD5 hex should be 32 chars, got %d", len(sign1)) + } +} + +func TestEasyPaySignExcludesSignAndSignType(t *testing.T) { + t.Parallel() + + pkey := "my_key" + base := map[string]string{ + "pid": "1001", + "type": "alipay", + } + withSign := map[string]string{ + "pid": "1001", + "type": "alipay", + "sign": "should_be_ignored", + "sign_type": "MD5", + } + + signBase := easyPaySign(base, pkey) + signWithExtra := easyPaySign(withSign, pkey) + + if signBase != signWithExtra { + t.Fatalf("sign and sign_type should be excluded: base=%q, withExtra=%q", signBase, signWithExtra) + } +} + +func TestEasyPaySignExcludesEmptyValues(t *testing.T) { + t.Parallel() + + pkey := "key123" + base := map[string]string{ + "pid": "1001", + "type": "alipay", + } + withEmpty := map[string]string{ + "pid": "1001", + "type": "alipay", + "device": "", + "clientip": "", + } + + signBase := easyPaySign(base, pkey) + signWithEmpty := easyPaySign(withEmpty, pkey) + + if signBase != signWithEmpty { + t.Fatalf("empty values should be excluded: base=%q, withEmpty=%q", signBase, signWithEmpty) + } +} + +func TestEasyPayVerifySignValid(t *testing.T) { + t.Parallel() + + params := map[string]string{ + "pid": "1001", + "type": "alipay", + "out_trade_no": "ORDER456", + "money": "25.00", + } + pkey := "secret" + + sign := easyPaySign(params, pkey) + + // Add sign to params (as would come in a real callback) + params["sign"] = sign + params["sign_type"] = "MD5" + + if !easyPayVerifySign(params, pkey, sign) { + t.Fatal("easyPayVerifySign should return true for a valid signature") + } +} + +func TestEasyPayVerifySignTampered(t *testing.T) { + t.Parallel() + + params := map[string]string{ + "pid": "1001", + "type": "alipay", + "out_trade_no": "ORDER789", + "money": "50.00", + } + pkey := "secret" + + sign := easyPaySign(params, pkey) + + // Tamper with the amount + params["money"] = "99.99" + + if easyPayVerifySign(params, pkey, sign) { + t.Fatal("easyPayVerifySign should return false for tampered params") + } +} + +func TestEasyPayVerifySignWrongKey(t *testing.T) { + t.Parallel() + + params := map[string]string{ + "pid": "1001", + "type": "wxpay", + } + + sign := easyPaySign(params, "correct_key") + + if easyPayVerifySign(params, "wrong_key", sign) { + t.Fatal("easyPayVerifySign should return false with wrong key") + } +} + +func TestEasyPaySignEmptyParams(t *testing.T) { + t.Parallel() + + sign := easyPaySign(map[string]string{}, "key123") + if sign == "" { + t.Fatal("easyPaySign with empty params should still produce a hash") + } + if len(sign) != 32 { + t.Fatalf("MD5 hex should be 32 chars, got %d", len(sign)) + } +} + +func TestEasyPaySignSortOrder(t *testing.T) { + t.Parallel() + + pkey := "test_key" + params1 := map[string]string{ + "a": "1", + "b": "2", + "c": "3", + } + params2 := map[string]string{ + "c": "3", + "a": "1", + "b": "2", + } + + sign1 := easyPaySign(params1, pkey) + sign2 := easyPaySign(params2, pkey) + + if sign1 != sign2 { + t.Fatalf("easyPaySign should be order-independent: %q != %q", sign1, sign2) + } +} + +func TestEasyPayVerifySignWrongSignValue(t *testing.T) { + t.Parallel() + + params := map[string]string{ + "pid": "1001", + "type": "alipay", + } + pkey := "key" + + if easyPayVerifySign(params, pkey, "00000000000000000000000000000000") { + t.Fatal("easyPayVerifySign should return false for an incorrect sign value") + } +} diff --git a/backend/internal/payment/registry_test.go b/backend/internal/payment/registry_test.go new file mode 100644 index 0000000000..6acfeb6abf --- /dev/null +++ b/backend/internal/payment/registry_test.go @@ -0,0 +1,234 @@ +package payment + +import ( + "context" + "fmt" + "sync" + "testing" +) + +// mockProvider implements the Provider interface for testing. +type mockProvider struct { + name string + key string + supportedTypes []PaymentType +} + +func (m *mockProvider) Name() string { return m.name } +func (m *mockProvider) ProviderKey() string { return m.key } +func (m *mockProvider) SupportedTypes() []PaymentType { return m.supportedTypes } +func (m *mockProvider) CreatePayment(_ context.Context, _ CreatePaymentRequest) (*CreatePaymentResponse, error) { + return nil, nil +} +func (m *mockProvider) QueryOrder(_ context.Context, _ string) (*QueryOrderResponse, error) { + return nil, nil +} +func (m *mockProvider) VerifyNotification(_ context.Context, _ string, _ map[string]string) (*PaymentNotification, error) { + return nil, nil +} +func (m *mockProvider) Refund(_ context.Context, _ RefundRequest) (*RefundResponse, error) { + return nil, nil +} + +func TestRegistryRegisterAndGetProvider(t *testing.T) { + t.Parallel() + r := NewRegistry() + + p := &mockProvider{ + name: "TestPay", + key: "testpay", + supportedTypes: []PaymentType{TypeAlipay, TypeWxpay}, + } + r.Register(p) + + got, err := r.GetProvider(TypeAlipay) + if err != nil { + t.Fatalf("GetProvider(alipay) error: %v", err) + } + if got.ProviderKey() != "testpay" { + t.Fatalf("GetProvider(alipay) key = %q, want %q", got.ProviderKey(), "testpay") + } + + got2, err := r.GetProvider(TypeWxpay) + if err != nil { + t.Fatalf("GetProvider(wxpay) error: %v", err) + } + if got2.ProviderKey() != "testpay" { + t.Fatalf("GetProvider(wxpay) key = %q, want %q", got2.ProviderKey(), "testpay") + } +} + +func TestRegistryGetProviderNotFound(t *testing.T) { + t.Parallel() + r := NewRegistry() + + _, err := r.GetProvider("nonexistent") + if err == nil { + t.Fatal("GetProvider for unregistered type should return error") + } +} + +func TestRegistryGetProviderByKey(t *testing.T) { + t.Parallel() + r := NewRegistry() + + p := &mockProvider{ + name: "EasyPay", + key: "easypay", + supportedTypes: []PaymentType{TypeAlipay}, + } + r.Register(p) + + got, err := r.GetProviderByKey("easypay") + if err != nil { + t.Fatalf("GetProviderByKey error: %v", err) + } + if got.Name() != "EasyPay" { + t.Fatalf("GetProviderByKey name = %q, want %q", got.Name(), "EasyPay") + } +} + +func TestRegistryGetProviderByKeyNotFound(t *testing.T) { + t.Parallel() + r := NewRegistry() + + _, err := r.GetProviderByKey("nonexistent") + if err == nil { + t.Fatal("GetProviderByKey for unknown key should return error") + } +} + +func TestRegistryGetProviderKeyUnknownType(t *testing.T) { + t.Parallel() + r := NewRegistry() + + key := r.GetProviderKey("unknown_type") + if key != "" { + t.Fatalf("GetProviderKey for unknown type should return empty, got %q", key) + } +} + +func TestRegistryGetProviderKeyKnownType(t *testing.T) { + t.Parallel() + r := NewRegistry() + + p := &mockProvider{ + name: "Stripe", + key: "stripe", + supportedTypes: []PaymentType{TypeStripe}, + } + r.Register(p) + + key := r.GetProviderKey(TypeStripe) + if key != "stripe" { + t.Fatalf("GetProviderKey(stripe) = %q, want %q", key, "stripe") + } +} + +func TestRegistrySupportedTypes(t *testing.T) { + t.Parallel() + r := NewRegistry() + + p1 := &mockProvider{ + name: "EasyPay", + key: "easypay", + supportedTypes: []PaymentType{TypeAlipay, TypeWxpay}, + } + p2 := &mockProvider{ + name: "Stripe", + key: "stripe", + supportedTypes: []PaymentType{TypeStripe}, + } + r.Register(p1) + r.Register(p2) + + types := r.SupportedTypes() + if len(types) != 3 { + t.Fatalf("SupportedTypes() len = %d, want 3", len(types)) + } + + typeSet := make(map[PaymentType]bool) + for _, tp := range types { + typeSet[tp] = true + } + for _, expected := range []PaymentType{TypeAlipay, TypeWxpay, TypeStripe} { + if !typeSet[expected] { + t.Fatalf("SupportedTypes() missing %q", expected) + } + } +} + +func TestRegistrySupportedTypesEmpty(t *testing.T) { + t.Parallel() + r := NewRegistry() + + types := r.SupportedTypes() + if len(types) != 0 { + t.Fatalf("SupportedTypes() on empty registry should be empty, got %d", len(types)) + } +} + +func TestRegistryOverwriteExisting(t *testing.T) { + t.Parallel() + r := NewRegistry() + + p1 := &mockProvider{ + name: "OldPay", + key: "old", + supportedTypes: []PaymentType{TypeAlipay}, + } + p2 := &mockProvider{ + name: "NewPay", + key: "new", + supportedTypes: []PaymentType{TypeAlipay}, + } + r.Register(p1) + r.Register(p2) + + got, err := r.GetProvider(TypeAlipay) + if err != nil { + t.Fatalf("GetProvider error: %v", err) + } + if got.Name() != "NewPay" { + t.Fatalf("expected overwritten provider, got %q", got.Name()) + } +} + +func TestRegistryConcurrentAccess(t *testing.T) { + t.Parallel() + r := NewRegistry() + + const goroutines = 50 + var wg sync.WaitGroup + wg.Add(goroutines * 2) + + // Concurrent writers + for i := 0; i < goroutines; i++ { + go func(idx int) { + defer wg.Done() + p := &mockProvider{ + name: fmt.Sprintf("Provider-%d", idx), + key: fmt.Sprintf("key-%d", idx), + supportedTypes: []PaymentType{PaymentType(fmt.Sprintf("type-%d", idx))}, + } + r.Register(p) + }(i) + } + + // Concurrent readers + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + _ = r.SupportedTypes() + _, _ = r.GetProvider("some-type") + _ = r.GetProviderKey("some-type") + }() + } + + wg.Wait() + + types := r.SupportedTypes() + if len(types) != goroutines { + t.Fatalf("after concurrent registration, expected %d types, got %d", goroutines, len(types)) + } +} diff --git a/backend/internal/service/payment_config_service_test.go b/backend/internal/service/payment_config_service_test.go new file mode 100644 index 0000000000..ffc5f24250 --- /dev/null +++ b/backend/internal/service/payment_config_service_test.go @@ -0,0 +1,183 @@ +package service + +import ( + "testing" +) + +func TestPcParseFloat(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + defaultVal float64 + expected float64 + }{ + {"empty string returns default", "", 1.0, 1.0}, + {"valid float", "3.14", 0, 3.14}, + {"valid integer as float", "42", 0, 42.0}, + {"invalid string returns default", "notanumber", 9.99, 9.99}, + {"zero value", "0", 5.0, 0}, + {"negative value", "-10.5", 0, -10.5}, + {"very large value", "99999999.99", 0, 99999999.99}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := pcParseFloat(tt.input, tt.defaultVal) + if got != tt.expected { + t.Fatalf("pcParseFloat(%q, %v) = %v, want %v", tt.input, tt.defaultVal, got, tt.expected) + } + }) + } +} + +func TestPcParseInt(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + defaultVal int + expected int + }{ + {"empty string returns default", "", 30, 30}, + {"valid int", "10", 0, 10}, + {"invalid string returns default", "abc", 5, 5}, + {"float string returns default", "3.14", 0, 0}, + {"zero value", "0", 99, 0}, + {"negative value", "-1", 0, -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := pcParseInt(tt.input, tt.defaultVal) + if got != tt.expected { + t.Fatalf("pcParseInt(%q, %v) = %v, want %v", tt.input, tt.defaultVal, got, tt.expected) + } + }) + } +} + +func TestParsePaymentConfig(t *testing.T) { + t.Parallel() + + svc := &PaymentConfigService{} + + t.Run("empty vals uses defaults", func(t *testing.T) { + t.Parallel() + cfg := svc.parsePaymentConfig(map[string]string{}) + if cfg.Enabled { + t.Fatal("expected Enabled=false by default") + } + if cfg.MinAmount != 1 { + t.Fatalf("expected MinAmount=1, got %v", cfg.MinAmount) + } + if cfg.MaxAmount != 99999999.99 { + t.Fatalf("expected MaxAmount=99999999.99, got %v", cfg.MaxAmount) + } + if cfg.OrderTimeoutMin != 30 { + t.Fatalf("expected OrderTimeoutMin=30, got %v", cfg.OrderTimeoutMin) + } + if cfg.MaxPendingOrders != 3 { + t.Fatalf("expected MaxPendingOrders=3, got %v", cfg.MaxPendingOrders) + } + if cfg.LoadBalanceStrategy != "round-robin" { + t.Fatalf("expected LoadBalanceStrategy=round-robin, got %q", cfg.LoadBalanceStrategy) + } + if len(cfg.EnabledTypes) != 0 { + t.Fatalf("expected empty EnabledTypes, got %v", cfg.EnabledTypes) + } + }) + + t.Run("all values populated", func(t *testing.T) { + t.Parallel() + vals := map[string]string{ + SettingPaymentEnabled: "true", + SettingMinRechargeAmount: "5.00", + SettingMaxRechargeAmount: "1000.00", + SettingDailyRechargeLimit: "5000.00", + SettingOrderTimeoutMinutes: "15", + SettingMaxPendingOrders: "5", + SettingEnabledPaymentTypes: "alipay,wxpay,stripe", + SettingBalancePayDisabled: "true", + SettingLoadBalanceStrategy: "least_amount", + SettingProductNamePrefix: "PRE", + SettingProductNameSuffix: "SUF", + } + cfg := svc.parsePaymentConfig(vals) + + if !cfg.Enabled { + t.Fatal("expected Enabled=true") + } + if cfg.MinAmount != 5 { + t.Fatalf("MinAmount = %v, want 5", cfg.MinAmount) + } + if cfg.MaxAmount != 1000 { + t.Fatalf("MaxAmount = %v, want 1000", cfg.MaxAmount) + } + if cfg.DailyLimit != 5000 { + t.Fatalf("DailyLimit = %v, want 5000", cfg.DailyLimit) + } + if cfg.OrderTimeoutMin != 15 { + t.Fatalf("OrderTimeoutMin = %v, want 15", cfg.OrderTimeoutMin) + } + if cfg.MaxPendingOrders != 5 { + t.Fatalf("MaxPendingOrders = %v, want 5", cfg.MaxPendingOrders) + } + if len(cfg.EnabledTypes) != 3 { + t.Fatalf("EnabledTypes len = %d, want 3", len(cfg.EnabledTypes)) + } + if cfg.EnabledTypes[0] != "alipay" || cfg.EnabledTypes[1] != "wxpay" || cfg.EnabledTypes[2] != "stripe" { + t.Fatalf("EnabledTypes = %v, want [alipay wxpay stripe]", cfg.EnabledTypes) + } + if !cfg.BalanceDisabled { + t.Fatal("expected BalanceDisabled=true") + } + if cfg.LoadBalanceStrategy != "least_amount" { + t.Fatalf("LoadBalanceStrategy = %q, want %q", cfg.LoadBalanceStrategy, "least_amount") + } + if cfg.ProductNamePrefix != "PRE" { + t.Fatalf("ProductNamePrefix = %q, want %q", cfg.ProductNamePrefix, "PRE") + } + if cfg.ProductNameSuffix != "SUF" { + t.Fatalf("ProductNameSuffix = %q, want %q", cfg.ProductNameSuffix, "SUF") + } + }) + + t.Run("enabled types with spaces are trimmed", func(t *testing.T) { + t.Parallel() + vals := map[string]string{ + SettingEnabledPaymentTypes: " alipay , wxpay ", + } + cfg := svc.parsePaymentConfig(vals) + if len(cfg.EnabledTypes) != 2 { + t.Fatalf("EnabledTypes len = %d, want 2", len(cfg.EnabledTypes)) + } + if cfg.EnabledTypes[0] != "alipay" || cfg.EnabledTypes[1] != "wxpay" { + t.Fatalf("EnabledTypes = %v, want [alipay wxpay]", cfg.EnabledTypes) + } + }) + + t.Run("empty enabled types string", func(t *testing.T) { + t.Parallel() + vals := map[string]string{ + SettingEnabledPaymentTypes: "", + } + cfg := svc.parsePaymentConfig(vals) + if len(cfg.EnabledTypes) != 0 { + t.Fatalf("expected empty EnabledTypes for empty string, got %v", cfg.EnabledTypes) + } + }) +} + +func TestGetBasePaymentType(t *testing.T) { + t.Parallel() + + // Import from the payment package is not needed since GetBasePaymentType + // is in a different package. This test covers parsePaymentConfig which + // is the core pure function in this service. + // Integration tests for DB-dependent methods would go in a separate file. +}