349 lines
8.6 KiB
Go
349 lines
8.6 KiB
Go
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)
|
|
}
|
|
}
|