Files
sub2api/backend/internal/service/payment_config_service_test.go
2026-04-20 17:39:57 +08:00

393 lines
12 KiB
Go

package service
import (
"context"
"database/sql"
"testing"
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/enttest"
"github.com/Wei-Shaw/sub2api/internal/payment"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
_ "modernc.org/sqlite"
)
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 != 0 {
t.Fatalf("expected MaxAmount=0 (no limit), 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 != payment.DefaultLoadBalanceStrategy {
t.Fatalf("expected LoadBalanceStrategy=%s, got %q", payment.DefaultLoadBalanceStrategy, 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("enabled types are normalized to visible methods and deduplicated", func(t *testing.T) {
t.Parallel()
vals := map[string]string{
SettingEnabledPaymentTypes: "alipay_direct, alipay, wxpay_direct, 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()
tests := []struct {
input string
expected string
}{
{payment.TypeEasyPay, payment.TypeEasyPay},
{payment.TypeStripe, payment.TypeStripe},
{payment.TypeCard, payment.TypeStripe},
{payment.TypeLink, payment.TypeStripe},
{payment.TypeAlipay, payment.TypeAlipay},
{payment.TypeAlipayDirect, payment.TypeAlipay},
{payment.TypeWxpay, payment.TypeWxpay},
{payment.TypeWxpayDirect, payment.TypeWxpay},
{"unknown", "unknown"},
{"", ""},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
t.Parallel()
got := payment.GetBasePaymentType(tt.input)
if got != tt.expected {
t.Fatalf("GetBasePaymentType(%q) = %q, want %q", tt.input, got, tt.expected)
}
})
}
}
func TestApplyVisibleMethodRoutingToEnabledTypes(t *testing.T) {
t.Parallel()
base := []string{"alipay", "wxpay", "stripe"}
vals := map[string]string{
SettingPaymentVisibleMethodAlipayEnabled: "true",
SettingPaymentVisibleMethodAlipaySource: VisibleMethodSourceOfficialAlipay,
SettingPaymentVisibleMethodWxpayEnabled: "true",
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
}
available := map[string]bool{
VisibleMethodSourceOfficialAlipay: true,
VisibleMethodSourceOfficialWechat: false,
}
got := applyVisibleMethodRoutingToEnabledTypes(base, vals, available)
want := []string{"alipay", "stripe"}
if len(got) != len(want) {
t.Fatalf("applyVisibleMethodRoutingToEnabledTypes len = %d, want %d (%v)", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("applyVisibleMethodRoutingToEnabledTypes[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
}
}
}
func TestApplyVisibleMethodRoutingAddsConfiguredVisibleMethod(t *testing.T) {
t.Parallel()
base := []string{"stripe"}
vals := map[string]string{
SettingPaymentVisibleMethodAlipayEnabled: "true",
SettingPaymentVisibleMethodAlipaySource: VisibleMethodSourceEasyPayAlipay,
}
available := map[string]bool{
VisibleMethodSourceEasyPayAlipay: true,
}
got := applyVisibleMethodRoutingToEnabledTypes(base, vals, available)
want := []string{"stripe", "alipay"}
if len(got) != len(want) {
t.Fatalf("applyVisibleMethodRoutingToEnabledTypes len = %d, want %d (%v)", len(got), len(want), got)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("applyVisibleMethodRoutingToEnabledTypes[%d] = %q, want %q (full=%v)", i, got[i], want[i], got)
}
}
}
func TestBuildVisibleMethodSourceAvailability(t *testing.T) {
t.Parallel()
instances := []*dbent.PaymentProviderInstance{
{ProviderKey: payment.TypeAlipay, SupportedTypes: "alipay"},
{ProviderKey: payment.TypeEasyPay, SupportedTypes: "wxpay_direct, alipay"},
{ProviderKey: payment.TypeWxpay, SupportedTypes: "wxpay_direct"},
}
got := buildVisibleMethodSourceAvailability(instances)
if !got[VisibleMethodSourceOfficialAlipay] {
t.Fatalf("expected %q to be available", VisibleMethodSourceOfficialAlipay)
}
if !got[VisibleMethodSourceEasyPayAlipay] {
t.Fatalf("expected %q to be available", VisibleMethodSourceEasyPayAlipay)
}
if !got[VisibleMethodSourceOfficialWechat] {
t.Fatalf("expected %q to be available", VisibleMethodSourceOfficialWechat)
}
if !got[VisibleMethodSourceEasyPayWechat] {
t.Fatalf("expected %q to be available", VisibleMethodSourceEasyPayWechat)
}
}
func TestGetPaymentConfigAppliesVisibleMethodRouting(t *testing.T) {
ctx := context.Background()
client := newPaymentConfigServiceTestClient(t)
_, err := client.PaymentProviderInstance.Create().
SetProviderKey(payment.TypeEasyPay).
SetName("EasyPay Alipay").
SetConfig("{}").
SetSupportedTypes("alipay").
SetEnabled(true).
Save(ctx)
if err != nil {
t.Fatalf("create easypay instance: %v", err)
}
svc := &PaymentConfigService{
entClient: client,
settingRepo: &paymentConfigSettingRepoStub{
values: map[string]string{
SettingEnabledPaymentTypes: "alipay,wxpay,stripe",
SettingPaymentVisibleMethodAlipayEnabled: "true",
SettingPaymentVisibleMethodAlipaySource: "easypay",
SettingPaymentVisibleMethodWxpayEnabled: "true",
SettingPaymentVisibleMethodWxpaySource: "wxpay",
},
},
}
cfg, err := svc.GetPaymentConfig(ctx)
if err != nil {
t.Fatalf("GetPaymentConfig returned error: %v", err)
}
want := []string{payment.TypeAlipay, payment.TypeStripe}
if len(cfg.EnabledTypes) != len(want) {
t.Fatalf("EnabledTypes len = %d, want %d (%v)", len(cfg.EnabledTypes), len(want), cfg.EnabledTypes)
}
for i := range want {
if cfg.EnabledTypes[i] != want[i] {
t.Fatalf("EnabledTypes[%d] = %q, want %q (full=%v)", i, cfg.EnabledTypes[i], want[i], cfg.EnabledTypes)
}
}
}
func newPaymentConfigServiceTestClient(t *testing.T) *dbent.Client {
t.Helper()
db, err := sql.Open("sqlite", "file:payment_config_service?mode=memory&cache=shared")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
t.Fatalf("enable foreign keys: %v", err)
}
drv := entsql.OpenDB(dialect.SQLite, db)
client := enttest.NewClient(t, enttest.WithOptions(dbent.Driver(drv)))
t.Cleanup(func() { _ = client.Close() })
return client
}
type paymentConfigSettingRepoStub struct {
values map[string]string
}
func (s *paymentConfigSettingRepoStub) Get(context.Context, string) (*Setting, error) {
return nil, nil
}
func (s *paymentConfigSettingRepoStub) GetValue(_ context.Context, key string) (string, error) {
return s.values[key], nil
}
func (s *paymentConfigSettingRepoStub) Set(context.Context, string, string) error { return nil }
func (s *paymentConfigSettingRepoStub) GetMultiple(_ context.Context, keys []string) (map[string]string, error) {
out := make(map[string]string, len(keys))
for _, key := range keys {
out[key] = s.values[key]
}
return out, nil
}
func (s *paymentConfigSettingRepoStub) SetMultiple(context.Context, map[string]string) error {
return nil
}
func (s *paymentConfigSettingRepoStub) GetAll(context.Context) (map[string]string, error) {
return s.values, nil
}
func (s *paymentConfigSettingRepoStub) Delete(context.Context, string) error { return nil }