feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
- 新增套餐系列管理 (CRUD + 状态切换) - 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态) - 清理 8 个废弃分佣模型及对应数据库表 - Package 模型新增建议成本价、建议售价、上架状态字段 - 完整的 Store/Service/Handler 三层实现 - 包含单元测试和集成测试 - 归档 add-package-module change - 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
This commit is contained in:
752
tests/integration/package_test.go
Normal file
752
tests/integration/package_test.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/routes"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgGorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutil"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type packageTestEnv struct {
|
||||
db *gorm.DB
|
||||
rdb *redis.Client
|
||||
tokenManager *auth.TokenManager
|
||||
app *fiber.App
|
||||
adminToken string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func setupPackageTestEnv(t *testing.T) *packageTestEnv {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("JUNHONG_DATABASE_HOST", "cxd.whcxd.cn")
|
||||
t.Setenv("JUNHONG_DATABASE_PORT", "16159")
|
||||
t.Setenv("JUNHONG_DATABASE_USER", "erp_pgsql")
|
||||
t.Setenv("JUNHONG_DATABASE_PASSWORD", "erp_2025")
|
||||
t.Setenv("JUNHONG_DATABASE_DBNAME", "junhong_cmp_test")
|
||||
t.Setenv("JUNHONG_REDIS_ADDRESS", "cxd.whcxd.cn")
|
||||
t.Setenv("JUNHONG_REDIS_PORT", "16299")
|
||||
t.Setenv("JUNHONG_REDIS_PASSWORD", "cpNbWtAaqgo1YJmbMp3h")
|
||||
t.Setenv("JUNHONG_JWT_SECRET_KEY", "test_secret_key_for_integration_tests")
|
||||
|
||||
cfg, err := config.Load()
|
||||
require.NoError(t, err)
|
||||
err = config.Set(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
zapLogger, _ := zap.NewDevelopment()
|
||||
|
||||
dsn := "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "cxd.whcxd.cn:16299",
|
||||
Password: "cpNbWtAaqgo1YJmbMp3h",
|
||||
DB: 15,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
err = rdb.Ping(ctx).Err()
|
||||
require.NoError(t, err)
|
||||
|
||||
testPrefix := fmt.Sprintf("test:%s:", t.Name())
|
||||
keys, _ := rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour)
|
||||
superAdmin := testutil.CreateSuperAdmin(t, db)
|
||||
adminToken, _ := testutil.GenerateTestToken(t, rdb, superAdmin, "web")
|
||||
|
||||
queueClient := queue.NewClient(rdb, zapLogger)
|
||||
|
||||
deps := &bootstrap.Dependencies{
|
||||
DB: db,
|
||||
Redis: rdb,
|
||||
Logger: zapLogger,
|
||||
TokenManager: tokenManager,
|
||||
QueueClient: queueClient,
|
||||
}
|
||||
|
||||
result, err := bootstrap.Bootstrap(deps)
|
||||
require.NoError(t, err)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: internalMiddleware.ErrorHandler(zapLogger),
|
||||
})
|
||||
|
||||
routes.RegisterRoutes(app, result.Handlers, result.Middlewares)
|
||||
|
||||
return &packageTestEnv{
|
||||
db: db,
|
||||
rdb: rdb,
|
||||
tokenManager: tokenManager,
|
||||
app: app,
|
||||
adminToken: adminToken,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *packageTestEnv) teardown() {
|
||||
e.db.Exec("DELETE FROM tb_package WHERE package_code LIKE 'TEST%'")
|
||||
e.db.Exec("DELETE FROM tb_package_series WHERE series_code LIKE 'TEST%'")
|
||||
|
||||
ctx := context.Background()
|
||||
testPrefix := fmt.Sprintf("test:%s:", e.t.Name())
|
||||
keys, _ := e.rdb.Keys(ctx, testPrefix+"*").Result()
|
||||
if len(keys) > 0 {
|
||||
e.rdb.Del(ctx, keys...)
|
||||
}
|
||||
|
||||
e.rdb.Close()
|
||||
}
|
||||
|
||||
// ==================== Part 1: 套餐系列 API 测试 ====================
|
||||
|
||||
func TestPackageSeriesAPI_Create(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesCode := fmt.Sprintf("TEST_SERIES_%d", timestamp)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"series_code": seriesCode,
|
||||
"series_name": "测试套餐系列",
|
||||
"description": "API集成测试创建的套餐系列",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/admin/package-series", bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, seriesCode, dataMap["series_code"])
|
||||
assert.Equal(t, "测试套餐系列", dataMap["series_name"])
|
||||
assert.Equal(t, float64(constants.StatusEnabled), dataMap["status"])
|
||||
|
||||
t.Logf("创建的套餐系列 ID: %v", dataMap["id"])
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_Get(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesCode := fmt.Sprintf("TEST_SERIES_%d", timestamp)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: "测试套餐系列",
|
||||
Description: "测试描述",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(series).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, seriesCode, dataMap["series_code"])
|
||||
assert.Equal(t, "测试套餐系列", dataMap["series_name"])
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_List(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesList := []*model.PackageSeries{
|
||||
{
|
||||
SeriesCode: fmt.Sprintf("TEST_LIST_%d_001", timestamp),
|
||||
SeriesName: "列表测试系列1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
{
|
||||
SeriesCode: fmt.Sprintf("TEST_LIST_%d_002", timestamp),
|
||||
SeriesName: "列表测试系列2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
}
|
||||
for _, s := range seriesList {
|
||||
require.NoError(t, env.db.Create(s).Error)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/admin/package-series?page=1&page_size=20", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_Update(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesCode := fmt.Sprintf("TEST_SERIES_%d", timestamp)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: "原始系列名称",
|
||||
Description: "原始描述",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(series).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"series_name": "更新后的系列名称",
|
||||
"description": "更新后的描述",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
req := httptest.NewRequest("PUT", url, bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "更新后的系列名称", dataMap["series_name"])
|
||||
assert.Equal(t, "更新后的描述", dataMap["description"])
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_UpdateStatus(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesCode := fmt.Sprintf("TEST_SERIES_%d", timestamp)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: "测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(series).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d/status", series.ID)
|
||||
req := httptest.NewRequest("PATCH", url, bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var updatedSeries model.PackageSeries
|
||||
env.db.First(&updatedSeries, series.ID)
|
||||
assert.Equal(t, constants.StatusDisabled, updatedSeries.Status)
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_Delete(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
seriesCode := fmt.Sprintf("TEST_SERIES_%d", timestamp)
|
||||
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: seriesCode,
|
||||
SeriesName: "测试系列",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(series).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
req := httptest.NewRequest("DELETE", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var deletedSeries model.PackageSeries
|
||||
err = env.db.First(&deletedSeries, series.ID).Error
|
||||
assert.Error(t, err, "删除后应查不到套餐系列")
|
||||
}
|
||||
|
||||
// ==================== Part 2: 套餐 API 测试 ====================
|
||||
|
||||
func TestPackageAPI_Create(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"package_code": packageCode,
|
||||
"package_name": "测试套餐",
|
||||
"package_type": "formal",
|
||||
"duration_months": 12,
|
||||
"price": 99900,
|
||||
"data_type": "real",
|
||||
"real_data_mb": 10240,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/admin/packages", bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, packageCode, dataMap["package_code"])
|
||||
assert.Equal(t, "测试套餐", dataMap["package_name"])
|
||||
assert.Equal(t, float64(constants.StatusEnabled), dataMap["status"])
|
||||
assert.Equal(t, float64(2), dataMap["shelf_status"]) // 默认下架
|
||||
|
||||
t.Logf("创建的套餐 ID: %v", dataMap["id"])
|
||||
}
|
||||
|
||||
func TestPackageAPI_UpdateStatus_DisableForceOffShelf(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
// 先创建套餐
|
||||
createBody := map[string]interface{}{
|
||||
"package_code": packageCode,
|
||||
"package_name": "测试套餐",
|
||||
"package_type": "formal",
|
||||
"duration_months": 12,
|
||||
"price": 99900,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(createBody)
|
||||
|
||||
createReq := httptest.NewRequest("POST", "/api/admin/packages", bytes.NewReader(jsonBody))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
createResp, err := env.app.Test(createReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
var createResult response.Response
|
||||
err = json.NewDecoder(createResp.Body).Decode(&createResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, createResult.Code)
|
||||
|
||||
dataMap := createResult.Data.(map[string]interface{})
|
||||
pkgID := uint(dataMap["id"].(float64))
|
||||
|
||||
// 先上架套餐
|
||||
shelfBody := map[string]interface{}{
|
||||
"shelf_status": 1,
|
||||
}
|
||||
shelfJsonBody, _ := json.Marshal(shelfBody)
|
||||
|
||||
shelfReq := httptest.NewRequest("PATCH", fmt.Sprintf("/api/admin/packages/%d/shelf", pkgID), bytes.NewReader(shelfJsonBody))
|
||||
shelfReq.Header.Set("Content-Type", "application/json")
|
||||
shelfReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
shelfResp, err := env.app.Test(shelfReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer shelfResp.Body.Close()
|
||||
|
||||
// 禁用套餐
|
||||
disableBody := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
disableJsonBody, _ := json.Marshal(disableBody)
|
||||
|
||||
disableReq := httptest.NewRequest("PATCH", fmt.Sprintf("/api/admin/packages/%d/status", pkgID), bytes.NewReader(disableJsonBody))
|
||||
disableReq.Header.Set("Content-Type", "application/json")
|
||||
disableReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
disableResp, err := env.app.Test(disableReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer disableResp.Body.Close()
|
||||
|
||||
var disableResult response.Response
|
||||
err = json.NewDecoder(disableResp.Body).Decode(&disableResult)
|
||||
require.NoError(t, err)
|
||||
t.Logf("禁用响应: 状态码=%d, 错误码=%d, 消息=%s", disableResp.StatusCode, disableResult.Code, disableResult.Message)
|
||||
require.Equal(t, 200, disableResp.StatusCode, "禁用套餐应该成功")
|
||||
require.Equal(t, 0, disableResult.Code, "禁用套餐应该返回成功")
|
||||
|
||||
// 验证禁用后自动下架
|
||||
var updatedPkg model.Package
|
||||
ctx := pkgGorm.SkipDataPermission(context.Background())
|
||||
require.NoError(t, env.db.WithContext(ctx).First(&updatedPkg, pkgID).Error)
|
||||
assert.Equal(t, constants.StatusDisabled, updatedPkg.Status, "套餐应该被禁用")
|
||||
assert.Equal(t, 2, updatedPkg.ShelfStatus, "禁用时应该强制下架")
|
||||
|
||||
t.Logf("禁用套餐后,状态: %d, 上架状态: %d", updatedPkg.Status, updatedPkg.ShelfStatus)
|
||||
}
|
||||
|
||||
func TestPackageAPI_UpdateShelfStatus_DisabledCannotOnShelf(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
// 先创建套餐
|
||||
createBody := map[string]interface{}{
|
||||
"package_code": packageCode,
|
||||
"package_name": "测试套餐",
|
||||
"package_type": "formal",
|
||||
"duration_months": 12,
|
||||
"price": 99900,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(createBody)
|
||||
|
||||
createReq := httptest.NewRequest("POST", "/api/admin/packages", bytes.NewReader(jsonBody))
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
createResp, err := env.app.Test(createReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer createResp.Body.Close()
|
||||
|
||||
var createResult response.Response
|
||||
err = json.NewDecoder(createResp.Body).Decode(&createResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, createResult.Code)
|
||||
|
||||
dataMap := createResult.Data.(map[string]interface{})
|
||||
pkgID := uint(dataMap["id"].(float64))
|
||||
|
||||
// 禁用套餐
|
||||
disableBody := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
disableJsonBody, _ := json.Marshal(disableBody)
|
||||
|
||||
disableReq := httptest.NewRequest("PATCH", fmt.Sprintf("/api/admin/packages/%d/status", pkgID), bytes.NewReader(disableJsonBody))
|
||||
disableReq.Header.Set("Content-Type", "application/json")
|
||||
disableReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
disableResp, err := env.app.Test(disableReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer disableResp.Body.Close()
|
||||
|
||||
var disableResult response.Response
|
||||
err = json.NewDecoder(disableResp.Body).Decode(&disableResult)
|
||||
require.NoError(t, err)
|
||||
t.Logf("禁用响应: 状态码=%d, 错误码=%d, 消息=%s", disableResp.StatusCode, disableResult.Code, disableResult.Message)
|
||||
require.Equal(t, 200, disableResp.StatusCode, "禁用套餐应该成功")
|
||||
require.Equal(t, 0, disableResult.Code, "禁用套餐应该返回成功")
|
||||
|
||||
// 尝试上架禁用的套餐
|
||||
shelfBody := map[string]interface{}{
|
||||
"shelf_status": 1,
|
||||
}
|
||||
shelfJsonBody, _ := json.Marshal(shelfBody)
|
||||
|
||||
shelfReq := httptest.NewRequest("PATCH", fmt.Sprintf("/api/admin/packages/%d/shelf", pkgID), bytes.NewReader(shelfJsonBody))
|
||||
shelfReq.Header.Set("Content-Type", "application/json")
|
||||
shelfReq.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
shelfResp, err := env.app.Test(shelfReq, -1)
|
||||
require.NoError(t, err)
|
||||
defer shelfResp.Body.Close()
|
||||
|
||||
// 应该返回错误
|
||||
var result response.Response
|
||||
err = json.NewDecoder(shelfResp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "禁用的套餐不能上架,应返回错误码")
|
||||
|
||||
// 验证套餐仍然是下架状态
|
||||
var unchangedPkg model.Package
|
||||
ctx := pkgGorm.SkipDataPermission(context.Background())
|
||||
require.NoError(t, env.db.WithContext(ctx).First(&unchangedPkg, pkgID).Error)
|
||||
assert.Equal(t, 2, unchangedPkg.ShelfStatus, "禁用的套餐应该保持下架状态")
|
||||
|
||||
t.Logf("尝试上架禁用套餐失败,错误码: %d, 消息: %s", result.Code, result.Message)
|
||||
}
|
||||
|
||||
func TestPackageAPI_Get(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(pkg).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
req := httptest.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, packageCode, dataMap["package_code"])
|
||||
assert.Equal(t, "测试套餐", dataMap["package_name"])
|
||||
}
|
||||
|
||||
func TestPackageAPI_List(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
pkgList := []*model.Package{
|
||||
{
|
||||
PackageCode: fmt.Sprintf("TEST_LIST_%d_001", timestamp),
|
||||
PackageName: "列表测试套餐1",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
{
|
||||
PackageCode: fmt.Sprintf("TEST_LIST_%d_002", timestamp),
|
||||
PackageName: "列表测试套餐2",
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
Price: 9990,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
}
|
||||
for _, p := range pkgList {
|
||||
require.NoError(t, env.db.Create(p).Error)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/api/admin/packages?page=1&page_size=20", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPackageAPI_Update(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "原始套餐名称",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(pkg).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"package_name": "更新后的套餐名称",
|
||||
"price": 119900,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
req := httptest.NewRequest("PUT", url, bytes.NewReader(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "更新后的套餐名称", dataMap["package_name"])
|
||||
assert.Equal(t, float64(119900), dataMap["price"])
|
||||
}
|
||||
|
||||
func TestPackageAPI_Delete(t *testing.T) {
|
||||
env := setupPackageTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Price: 99900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.db.Create(pkg).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
req := httptest.NewRequest("DELETE", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+env.adminToken)
|
||||
|
||||
resp, err := env.app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var deletedPkg model.Package
|
||||
err = env.db.First(&deletedPkg, pkg.ID).Error
|
||||
assert.Error(t, err, "删除后应查不到套餐")
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutil"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -30,7 +31,7 @@ import (
|
||||
// shopManagementTestEnv 商户管理测试环境
|
||||
type shopManagementTestEnv struct {
|
||||
tx *gorm.DB
|
||||
rdb *redis.Client
|
||||
rdb *redis.Client
|
||||
tokenManager *auth.TokenManager
|
||||
app *fiber.App
|
||||
adminToken string
|
||||
@@ -113,7 +114,7 @@ func setupShopManagementTestEnv(t *testing.T) *shopManagementTestEnv {
|
||||
|
||||
return &shopManagementTestEnv{
|
||||
tx: tx,
|
||||
rdb: rdb,
|
||||
rdb: rdb,
|
||||
tokenManager: tokenManager,
|
||||
app: app,
|
||||
adminToken: adminToken,
|
||||
@@ -146,7 +147,7 @@ func TestShopManagement_CreateShop(t *testing.T) {
|
||||
ShopName: "测试商户",
|
||||
ShopCode: "TEST001",
|
||||
InitUsername: "testuser",
|
||||
InitPhone: "13800138000",
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
@@ -191,12 +192,11 @@ func TestShopManagement_CreateShop_DuplicateCode(t *testing.T) {
|
||||
env := setupShopManagementTestEnv(t)
|
||||
defer env.teardown()
|
||||
|
||||
// 通过 API 创建第一个商户
|
||||
firstReq := dto.CreateShopRequest{
|
||||
ShopName: "商户1",
|
||||
ShopCode: "DUP001",
|
||||
InitUsername: "dupuser1",
|
||||
InitPhone: "13800138101",
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
firstBody, _ := json.Marshal(firstReq)
|
||||
@@ -210,12 +210,11 @@ func TestShopManagement_CreateShop_DuplicateCode(t *testing.T) {
|
||||
|
||||
require.Equal(t, 0, firstResult.Code, "第一个商户应该创建成功")
|
||||
|
||||
// 尝试创建编码重复的商户
|
||||
reqBody := dto.CreateShopRequest{
|
||||
ShopName: "商户2",
|
||||
ShopCode: "DUP001", // 使用相同编码
|
||||
ShopCode: "DUP001",
|
||||
InitUsername: "dupuser2",
|
||||
InitPhone: "13800138102",
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// EmailPayload 邮件任务载荷(测试用)
|
||||
type EmailPayload struct {
|
||||
RequestID string `json:"request_id"`
|
||||
To string `json:"to"`
|
||||
@@ -23,21 +22,21 @@ type EmailPayload struct {
|
||||
CC []string `json:"cc,omitempty"`
|
||||
}
|
||||
|
||||
// TestTaskSubmit 测试任务提交
|
||||
func TestTaskSubmit(t *testing.T) {
|
||||
// 创建 Redis 客户端
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
// 清理测试数据
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
// 创建 Asynq 客户端
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
@@ -66,20 +65,21 @@ func TestTaskSubmit(t *testing.T) {
|
||||
assert.Equal(t, constants.DefaultRetryMax, info.MaxRetry)
|
||||
}
|
||||
|
||||
// TestTaskPriority 测试任务优先级
|
||||
func TestTaskPriority(t *testing.T) {
|
||||
// 创建 Redis 客户端
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
// 创建 Asynq 客户端
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
@@ -113,18 +113,21 @@ func TestTaskPriority(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaskRetry 测试任务重试机制
|
||||
func TestTaskRetry(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
@@ -150,20 +153,22 @@ func TestTaskRetry(t *testing.T) {
|
||||
assert.Equal(t, 30*time.Second, info.Timeout)
|
||||
}
|
||||
|
||||
// TestTaskIdempotency 测试任务幂等性键
|
||||
func TestTaskIdempotency(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
requestID := "idempotent-test-001"
|
||||
requestID := "idempotent-test-" + time.Now().Format("20060102150405.000")
|
||||
lockKey := constants.RedisTaskLockKey(requestID)
|
||||
rdb.Del(ctx, lockKey)
|
||||
t.Cleanup(func() { rdb.Del(ctx, lockKey) })
|
||||
|
||||
// 第一次设置锁(模拟任务开始执行)
|
||||
result, err := rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result, "第一次设置锁应该成功")
|
||||
@@ -185,15 +190,16 @@ func TestTaskIdempotency(t *testing.T) {
|
||||
assert.LessOrEqual(t, ttl.Hours(), 24.0)
|
||||
}
|
||||
|
||||
// TestTaskStatusTracking 测试任务状态跟踪
|
||||
func TestTaskStatusTracking(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
taskID := "task-123456"
|
||||
statusKey := constants.RedisTaskStatusKey(taskID)
|
||||
@@ -217,18 +223,21 @@ func TestTaskStatusTracking(t *testing.T) {
|
||||
assert.Greater(t, ttl.Hours(), 24.0*6)
|
||||
}
|
||||
|
||||
// TestQueueInspection 测试队列检查
|
||||
func TestQueueInspection(t *testing.T) {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = rdb.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
rdb.FlushDB(ctx)
|
||||
cleanTestKeys(t, rdb, ctx)
|
||||
|
||||
client := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
@@ -249,9 +258,10 @@ func TestQueueInspection(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 创建 Inspector 检查队列
|
||||
inspector := asynq.NewInspector(asynq.RedisClientOpt{
|
||||
Addr: "localhost:6379",
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
@@ -262,7 +272,6 @@ func TestQueueInspection(t *testing.T) {
|
||||
assert.Equal(t, 0, info.Active)
|
||||
}
|
||||
|
||||
// TestTaskSerialization 测试任务序列化
|
||||
func TestTaskSerialization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -291,17 +300,14 @@ func TestTaskSerialization(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 序列化
|
||||
payloadBytes, err := sonic.Marshal(tt.payload)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, payloadBytes)
|
||||
|
||||
// 反序列化
|
||||
var decoded EmailPayload
|
||||
err = sonic.Unmarshal(payloadBytes, &decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证
|
||||
assert.Equal(t, tt.payload.RequestID, decoded.RequestID)
|
||||
assert.Equal(t, tt.payload.To, decoded.To)
|
||||
assert.Equal(t, tt.payload.Subject, decoded.Subject)
|
||||
@@ -310,3 +316,19 @@ func TestTaskSerialization(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func cleanTestKeys(t *testing.T, rdb *redis.Client, ctx context.Context) {
|
||||
t.Helper()
|
||||
prefix := "test:task:" + t.Name() + ":"
|
||||
keys, err := rdb.Keys(ctx, prefix+"*").Result()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
rdb.Del(ctx, keys...)
|
||||
}
|
||||
asynqKeys, _ := rdb.Keys(ctx, "asynq:*").Result()
|
||||
if len(asynqKeys) > 0 {
|
||||
rdb.Del(ctx, asynqKeys...)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user