package payment import ( "testing" "hfb_sys/backend/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // setupPaymentTestDB 创建测试数据库 func setupPaymentTestDB(t *testing.T) *gorm.DB { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("无法创建测试数据库: %v", err) } if err := db.AutoMigrate( &model.PaymentOrder{}, &model.RentalOrder{}, &model.User{}, ); err != nil { t.Fatalf("数据库迁移失败: %v", err) } return db } // TestCanReusePaymentWithSameProvider 测试相同渠道可复用 func TestCanReusePaymentWithSameProvider(t *testing.T) { payment := model.PaymentOrder{ Status: "paying", Provider: "lakala", MerchantID: "M123", } config := runtimePaymentConfig{ Provider: "lakala", MerchantID: "M123", } if !canReuseOrderPayment(payment, config) { t.Fatal("should reuse payment with same provider and merchant") } } // TestCannotReusePaymentWithDifferentProvider 测试不同渠道不可复用 func TestCannotReusePaymentWithDifferentProvider(t *testing.T) { payment := model.PaymentOrder{ Status: "paying", Provider: "lakala", MerchantID: "M123", } config := runtimePaymentConfig{ Provider: "leshua", // 不同渠道 MerchantID: "M456", } if canReuseOrderPayment(payment, config) { t.Fatal("should not reuse payment with different provider") } } // TestPaymentStatusTransitions 测试支付单状态转换 func TestPaymentStatusTransitionsValid(t *testing.T) { validTransitions := map[string][]string{ "pending": {"paying", "closed"}, "paying": {"paid", "failed", "closed"}, "paid": {"refunding", "refunded"}, "failed": {}, // 终态 "closed": {}, // 终态 "refunded": {}, // 终态 } for from, toList := range validTransitions { if from == "" { t.Fatal("payment status should not be empty") } for _, to := range toList { if to == "" { t.Fatal("transition target should not be empty") } } } } // TestRefundBizTypeCoverage 测试退款业务类型覆盖 func TestRefundBizTypeCoverage(t *testing.T) { expected := []string{ "cancel_refund", "admin_close_refund", "admin_refund", "checkout_refund", "deposit_refund", "rent_refund", "arbitration_refund", } if len(refundBizTypes) != len(expected) { t.Fatalf("refundBizTypes count = %d, want %d", len(refundBizTypes), len(expected)) } // 验证每个预期类型都存在 typeMap := make(map[string]bool) for _, bizType := range refundBizTypes { typeMap[bizType] = true } for _, exp := range expected { if !typeMap[exp] { t.Fatalf("missing refund biz type: %s", exp) } } } // TestPaymentAmountValidation 测试支付金额验证 func TestPaymentAmountMustBePositive(t *testing.T) { testCases := []struct { name string amount int64 wantValid bool }{ {"正数金额", 10000, true}, {"零金额", 0, false}, {"负数金额", -1000, false}, {"最小金额", 1, true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { isValid := tc.amount > 0 if isValid != tc.wantValid { t.Fatalf("amount %d validation = %v, want %v", tc.amount, isValid, tc.wantValid) } }) } } // TestPaymentProviderValidation 测试支付渠道验证 func TestPaymentProviderMustNotBeEmpty(t *testing.T) { testCases := []struct { name string provider string wantValid bool }{ {"lakala 渠道", "lakala", true}, {"leshua 渠道", "leshua", true}, {"mock 渠道", "mock", true}, {"空渠道", "", false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { isValid := tc.provider != "" if isValid != tc.wantValid { t.Fatalf("provider %q validation = %v, want %v", tc.provider, isValid, tc.wantValid) } }) } } // TestRuntimeConfigIsMockMode 测试运行时配置 mock 模式 func TestRuntimeConfigIsMockModeTrue(t *testing.T) { config := runtimePaymentConfig{ Provider: "mock", } if !config.isMockMode() { t.Fatal("should be mock mode when provider is 'mock'") } } func TestRuntimeConfigIsMockModeFalse(t *testing.T) { providers := []string{"lakala", "leshua", "alipay", "wechat"} for _, provider := range providers { config := runtimePaymentConfig{ Provider: provider, } if config.isMockMode() { t.Fatalf("should not be mock mode when provider is %q", provider) } } } // TestChannelSourceValidation 测试渠道来源验证 func TestChannelSourceMustBeValid(t *testing.T) { validSources := []string{ channelSourceCreate, channelSourceQuery, channelSourceNotify, channelSourceMock, } expectedValues := []string{"create", "query", "notify", "mock"} for i, source := range validSources { if source != expectedValues[i] { t.Fatalf("channelSource[%d] = %q, want %q", i, source, expectedValues[i]) } } } // TestPaymentOrderFields 测试支付单字段完整性 func TestPaymentOrderRequiredFields(t *testing.T) { payment := model.PaymentOrder{ PaymentNo: "PAY123456", OrderNo: "ORD123456", OrderID: 100, UserID: 1, AmountCent: 10000, Status: "paying", Provider: "lakala", MerchantID: "M123", BizType: "order_pay", } // 验证必填字段 if payment.PaymentNo == "" { t.Fatal("PaymentNo should not be empty") } if payment.OrderNo == "" { t.Fatal("OrderNo should not be empty") } if payment.OrderID == 0 { t.Fatal("OrderID should not be zero") } if payment.UserID == 0 { t.Fatal("UserID should not be zero") } if payment.AmountCent <= 0 { t.Fatal("AmountCent should be positive") } if payment.Status == "" { t.Fatal("Status should not be empty") } if payment.Provider == "" { t.Fatal("Provider should not be empty") } } // TestRefundAmountValidation 测试退款金额验证 func TestRefundAmountMustNotExceedOriginal(t *testing.T) { testCases := []struct { name string originalAmount int64 refundAmount int64 wantValid bool }{ {"部分退款", 10000, 5000, true}, {"全额退款", 10000, 10000, true}, {"超额退款", 10000, 15000, false}, {"零退款", 10000, 0, false}, {"负数退款", 10000, -1000, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { isValid := tc.refundAmount > 0 && tc.refundAmount <= tc.originalAmount if isValid != tc.wantValid { t.Fatalf("refund %d from %d validation = %v, want %v", tc.refundAmount, tc.originalAmount, isValid, tc.wantValid) } }) } } // TestPaymentNotifyResultStructure 测试支付回调结果结构 func TestNotifyResultHasRequiredFields(t *testing.T) { result := NotifyResult{ OK: true, Message: "payment success", } if result.Message == "" { t.Fatal("NotifyResult.Message should not be empty") } } func TestStartRefundDoesNotCreateNewOrderWhenFailedRefundExists(t *testing.T) { db := setupPaymentTestDB(t) repo := NewRepository(db, nil, nil) order := model.RentalOrder{ ID: 1001, OrderNo: "ORD202606140001", RenterID: 11, RentAmountCent: 800, DepositAmountCent: 200, RefundStatus: "failed", RefundAmountCent: 1000, Status: "closed", } if err := db.Create(&order).Error; err != nil { t.Fatalf("create order failed: %v", err) } original := model.PaymentOrder{ PaymentNo: "PAY202606140001", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID, Provider: "mock", ThirdOrderID: "PAY202606140001", ProviderOrderID: "MOCKPAY202606140001", AmountCent: 1000, BizType: "order_pay", Status: "paid", } if err := db.Create(&original).Error; err != nil { t.Fatalf("create original payment failed: %v", err) } existingRefund := model.PaymentOrder{ PaymentNo: "PAY202606140002", OrderID: order.ID, OrderNo: order.OrderNo, UserID: order.RenterID, Provider: "mock", ThirdOrderID: "REF202606140002", AmountCent: 1000, BizType: "admin_refund", Status: "failed", } if err := db.Create(&existingRefund).Error; err != nil { t.Fatalf("create existing refund failed: %v", err) } dto, err := repo.StartRefund(t.Context(), order.ID, 1000, "admin_refund", "后台人工退款") if err != nil { t.Fatalf("StartRefund() error = %v", err) } if dto.ID != existingRefund.ID { t.Fatalf("StartRefund() returned payment id %d, want existing %d", dto.ID, existingRefund.ID) } var count int64 if err := db.Model(&model.PaymentOrder{}). Where("order_id = ? AND biz_type = ?", order.ID, "admin_refund"). Count(&count).Error; err != nil { t.Fatalf("count refund orders failed: %v", err) } if count != 1 { t.Fatalf("refund order count = %d, want 1", count) } }