mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-13 00:01:35 +08:00
test: add payment system unit tests (69 test cases)
- fee_test.go: 14 cases for CalculatePayAmount precision/rounding - crypto_test.go: 9 cases for AES-256-GCM encrypt/decrypt round-trip - registry_test.go: 10 cases including concurrent thread safety - easypay_sign_test.go: 9 cases for MD5 signing/verification - load_balancer_test.go: 10 cases for containsType utility - payment_config_service_test.go: 17 cases for config parsing
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
Reference in New Issue
Block a user