Files
junhong_cmp_fiber/internal/routes/shop_integration_test.go

638 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package routes
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/bytedance/sonic"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
"github.com/break/junhong_cmp_fiber/internal/model"
shopService "github.com/break/junhong_cmp_fiber/internal/service/shop"
shopCommissionService "github.com/break/junhong_cmp_fiber/internal/service/shop_commission"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/auth"
"github.com/break/junhong_cmp_fiber/pkg/config"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/database"
"github.com/break/junhong_cmp_fiber/pkg/errors"
pkgMiddleware "github.com/break/junhong_cmp_fiber/pkg/middleware"
)
type shopTestResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data shopTestPageData `json:"data"`
}
type shopTestPageData struct {
Items []map[string]any `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
type shopTestEnv struct {
app *fiber.App
db *gorm.DB
redis *redis.Client
tokenManager *auth.TokenManager
}
// TestShopListValidatesAllQueryParameters 验证店铺列表会完整校验所有查询参数。
func TestShopListValidatesAllQueryParameters(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeSuperAdmin,
Username: "ur60-super-admin",
})
testCases := []struct {
name string
query string
}{
{name: "页码小于一", query: "page=0"},
{name: "每页数量小于一", query: "page_size=0"},
{name: "每页数量超过上限", query: "page_size=101"},
{name: "店铺层级非法", query: "level=8"},
{name: "店铺状态非法", query: "status=2"},
{name: "店铺名称过长", query: "shop_name=" + strings.Repeat("店", 101)},
{name: "店铺编号过长", query: "shop_code=" + strings.Repeat("A", 51)},
{name: "参数解析失败", query: "parent_id=invalid"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
status, body := env.request(t, http.MethodGet, "/api/admin/shops?"+testCase.query, token)
if status != http.StatusBadRequest {
t.Fatalf("期望 HTTP 400实际为 %d响应%s", status, body)
}
var response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析响应失败:%v", err)
}
if response.Code != errors.CodeInvalidParam || response.Msg != "参数验证失败" || response.Data != nil {
t.Fatalf("参数错误响应不符合契约:%s", body)
}
if !strings.Contains(string(body), "timestamp") {
t.Fatalf("参数错误响应缺少时间戳:%s", body)
}
if strings.Contains(string(body), "validation") || strings.Contains(string(body), "strconv") {
t.Fatalf("参数错误响应泄露底层细节:%s", body)
}
})
}
}
// TestShopListUsesNormalizedAndExplicitPagination 验证默认分页归一化且显式分页保持不变。
func TestShopListUsesNormalizedAndExplicitPagination(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeSuperAdmin,
Username: "ur60-super-admin",
})
testCases := []struct {
name string
query string
expectedPage int
expectedSize int
}{
{name: "默认分页", expectedPage: constants.DefaultPage, expectedSize: constants.DefaultPageSize},
{name: "显式分页", query: "?page=2&page_size=7&shop_name=ur60-no-match", expectedPage: 2, expectedSize: 7},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
status, body := env.request(t, http.MethodGet, "/api/admin/shops"+testCase.query, token)
if status != http.StatusOK {
t.Fatalf("期望 HTTP 200实际为 %d响应%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析响应失败:%v", err)
}
if response.Code != errors.CodeSuccess || response.Data.Page != testCase.expectedPage || response.Data.Size != testCase.expectedSize {
t.Fatalf("分页响应不符合契约:%s", body)
}
})
}
}
// TestShopListKeepsAuthenticationContract 验证店铺列表保持既有认证错误契约。
func TestShopListKeepsAuthenticationContract(t *testing.T) {
env := newShopTestEnv(t)
testCases := []struct {
name string
token string
}{
{name: "未认证"},
{name: "无效令牌", token: "ur60-invalid-token"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
status, body := env.request(t, http.MethodGet, "/api/admin/shops", testCase.token)
if status != http.StatusUnauthorized {
t.Fatalf("期望 HTTP 401实际为 %d响应%s", status, body)
}
if !strings.Contains(string(body), "timestamp") {
t.Fatalf("认证错误响应缺少时间戳:%s", body)
}
})
}
}
// TestEnterpriseCannotAccessCoreShopManagementRoutes 验证企业账号无法访问五个核心店铺管理入口。
func TestEnterpriseCannotAccessCoreShopManagementRoutes(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeEnterprise,
EnterpriseID: uniqueTestID(),
Username: "ur60-enterprise",
})
testCases := []struct {
name string
method string
path string
}{
{name: "店铺列表", method: http.MethodGet, path: "/api/admin/shops"},
{name: "创建店铺", method: http.MethodPost, path: "/api/admin/shops"},
{name: "更新店铺", method: http.MethodPut, path: "/api/admin/shops/1"},
{name: "删除店铺", method: http.MethodDelete, path: "/api/admin/shops/1"},
{name: "联级查询", method: http.MethodGet, path: "/api/admin/shops/cascade"},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
status, body := env.request(t, testCase.method, testCase.path, token)
if status != http.StatusForbidden {
t.Fatalf("期望 HTTP 403实际为 %d响应%s", status, body)
}
var response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析响应失败:%v", err)
}
if response.Code != errors.CodeForbidden || response.Msg != constants.ShopManagementForbiddenMessage || response.Data != nil {
t.Fatalf("企业账号禁止响应不符合契约:%s", body)
}
if !strings.Contains(string(body), "timestamp") {
t.Fatalf("企业账号禁止响应缺少时间戳:%s", body)
}
})
}
}
// TestCoreShopRestrictionDoesNotAffectOtherShopRoutes 验证核心店铺权限限制不影响其他店铺前缀路由。
func TestCoreShopRestrictionDoesNotAffectOtherShopRoutes(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeEnterprise,
EnterpriseID: uniqueTestID(),
Username: "ur60-enterprise",
})
paths := []string{
"/api/admin/shops/1/roles",
"/api/admin/shops/fund-summary",
"/api/admin/shops/1/withdrawal-requests",
"/api/admin/shops/1/commission-records",
"/api/admin/shops/1/main-wallet/transactions",
}
for _, path := range paths {
status, body := env.request(t, http.MethodGet, path, token)
if status == http.StatusForbidden && strings.Contains(string(body), constants.ShopManagementForbiddenMessage) {
t.Fatalf("核心店铺管理拦截误伤独立路由 %s%s", path, body)
}
}
}
// TestNonEnterpriseAccountsKeepCoreShopListAccess 验证非企业账号保留核心店铺列表访问能力。
func TestNonEnterpriseAccountsKeepCoreShopListAccess(t *testing.T) {
env := newShopTestEnv(t)
testCases := []struct {
name string
userType int
shopID uint
}{
{name: "超级管理员", userType: constants.UserTypeSuperAdmin},
{name: "平台账号", userType: constants.UserTypePlatform},
{name: "代理账号", userType: constants.UserTypeAgent, shopID: 1},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: testCase.userType,
ShopID: testCase.shopID,
Username: "ur60-allowed-user",
})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?page=1&page_size=1", token)
if status != http.StatusOK {
t.Fatalf("期望保留列表访问能力,实际 HTTP %d响应%s", status, body)
}
})
}
}
// TestShopListFiltersContactPhoneExactly 验证联系电话精确匹配、AND 组合、重复号码、排序及软删除语义。
func TestShopListFiltersContactPhoneExactly(t *testing.T) {
env := newShopTestEnv(t)
phone := "13800000000"
parent := env.createShop(t, "UR60父店", phone, nil, 1, time.Now().Add(-4*time.Minute))
newer := env.createShop(t, "UR60目标新店", phone, &parent.ID, 2, time.Now().Add(-time.Minute))
older := env.createShop(t, "UR60目标旧店", phone, &parent.ID, 2, time.Now().Add(-2*time.Minute))
env.createShop(t, "UR60相似号码", "13800000001", &parent.ID, 2, time.Now())
deleted := env.createShop(t, "UR60软删除店", phone, &parent.ID, 2, time.Now().Add(time.Minute))
if err := env.db.Delete(deleted).Error; err != nil {
t.Fatalf("软删除测试店铺失败:%v", err)
}
token := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypeSuperAdmin,
Username: "ur60-super-admin",
})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标&parent_id="+strconv.FormatUint(uint64(parent.ID), 10)+"&level=2&status=1", token)
if status != http.StatusOK {
t.Fatalf("联系电话组合查询失败HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析联系电话查询响应失败:%v", err)
}
if response.Data.Total != 2 || len(response.Data.Items) != 2 {
t.Fatalf("联系电话组合查询应返回两条可见记录:%s", body)
}
if uint(response.Data.Items[0]["id"].(float64)) != newer.ID || uint(response.Data.Items[1]["id"].(float64)) != older.ID {
t.Fatalf("联系电话查询未保持 created_at DESC%s", body)
}
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000001&shop_code="+newer.ShopCode, token)
if status != http.StatusOK {
t.Fatalf("联系电话精确性查询失败HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析联系电话精确性响应失败:%v", err)
}
if response.Data.Total != 0 {
t.Fatalf("联系电话必须与其他条件按 AND 精确匹配:%s", body)
}
platformToken := env.newToken(t, auth.TokenInfo{
UserID: uniqueTestID(),
UserType: constants.UserTypePlatform,
Username: "ur60-platform",
})
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone+"&shop_name=UR60目标", platformToken)
if status != http.StatusOK {
t.Fatalf("平台账号联系电话查询失败HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析平台账号联系电话响应失败:%v", err)
}
if response.Data.Total != 2 {
t.Fatalf("平台账号联系电话查询结果不符合契约:%s", body)
}
}
// TestShopListContactPhoneKeepsEmptyAndPaginationSemantics 验证空电话、空结果和显式分页契约。
func TestShopListContactPhoneKeepsEmptyAndPaginationSemantics(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=&page=2&page_size=3", token)
if status != http.StatusOK {
t.Fatalf("空联系电话不应改变列表语义HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析空联系电话响应失败:%v", err)
}
if response.Data.Page != 2 || response.Data.Size != 3 {
t.Fatalf("联系电话筛选不应重置显式分页:%s", body)
}
status, body = env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=19999999999", token)
if status != http.StatusOK {
t.Fatalf("无匹配联系电话应返回成功空分页HTTP %d%s", status, body)
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析空结果响应失败:%v", err)
}
if response.Data.Total != 0 || len(response.Data.Items) != 0 {
t.Fatalf("无匹配联系电话响应不符合空分页契约:%s", body)
}
}
// TestShopListRejectsInvalidContactPhoneBeforeQuery 验证非法联系电话在执行店铺查询前被拒绝。
func TestShopListRejectsInvalidContactPhoneBeforeQuery(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
var shopQueryCount atomic.Int64
callbackName := fmt.Sprintf("ur60:count_shop_queries:%d", time.Now().UnixNano())
if err := env.db.Callback().Query().Before("gorm:query").Register(callbackName, func(db *gorm.DB) {
if db.Statement != nil && db.Statement.Table == (model.Shop{}).TableName() {
shopQueryCount.Add(1)
}
}); err != nil {
t.Fatalf("注册店铺查询计数回调失败:%v", err)
}
t.Cleanup(func() { _ = env.db.Callback().Query().Remove(callbackName) })
invalidPhones := []string{
"1380000000", "138000000000", "1380000000A", "+8613800000000",
"138-0000-0000", " 13800000000", "",
}
for _, phone := range invalidPhones {
shopQueryCount.Store(0)
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+url.QueryEscape(phone), token)
if status != http.StatusBadRequest || shopQueryCount.Load() != 0 {
t.Fatalf("非法联系电话应在查询前返回 400phone=%q, queries=%d, body=%s", phone, shopQueryCount.Load(), body)
}
}
}
// TestAgentContactPhoneSearchKeepsShopScope 验证代理账号无法通过联系电话越过店铺层级范围。
func TestAgentContactPhoneSearchKeepsShopScope(t *testing.T) {
env := newShopTestEnv(t)
phone := "13700000000"
root := env.createShop(t, "UR60代理根店", phone, nil, 1, time.Now().Add(-3*time.Minute))
child := env.createShop(t, "UR60代理下级", phone, &root.ID, 2, time.Now().Add(-2*time.Minute))
env.createShop(t, "UR60范围外店铺", phone, nil, 1, time.Now().Add(-time.Minute))
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeAgent, ShopID: root.ID, Username: "ur60-agent"})
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone="+phone, token)
if status != http.StatusOK {
t.Fatalf("代理联系电话查询失败HTTP %d%s", status, body)
}
var response shopTestResponse
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析代理联系电话响应失败:%v", err)
}
if response.Data.Total != 2 {
t.Fatalf("代理联系电话查询应只返回自身及下级:%s", body)
}
returned := map[uint]bool{}
for _, item := range response.Data.Items {
returned[uint(item["id"].(float64))] = true
}
if !returned[root.ID] || !returned[child.ID] {
t.Fatalf("代理联系电话查询缺少范围内店铺:%s", body)
}
}
// TestShopListDatabaseFailureIsSanitized 验证数据库错误通过统一 500 响应脱敏。
func TestShopListDatabaseFailureIsSanitized(t *testing.T) {
env := newShopTestEnv(t)
token := env.newToken(t, auth.TokenInfo{UserID: uniqueTestID(), UserType: constants.UserTypeSuperAdmin, Username: "ur60-super-admin"})
sqlDB, err := env.db.DB()
if err != nil {
t.Fatalf("获取测试数据库连接失败:%v", err)
}
if err := sqlDB.Close(); err != nil {
t.Fatalf("关闭测试数据库连接失败:%v", err)
}
status, body := env.request(t, http.MethodGet, "/api/admin/shops?contact_phone=13800000000", token)
if status != http.StatusInternalServerError {
t.Fatalf("数据库失败应返回 HTTP 500实际 %d%s", status, body)
}
var response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data any `json:"data"`
}
if err := sonic.Unmarshal(body, &response); err != nil {
t.Fatalf("解析数据库错误响应失败:%v", err)
}
if response.Code != errors.CodeInternalError || response.Msg != "内部服务器错误" || response.Data != nil {
t.Fatalf("数据库错误响应不符合脱敏契约:%s", body)
}
for _, leaked := range []string{"sql", "postgres", "host", "driver", "database is closed"} {
if strings.Contains(strings.ToLower(string(body)), leaked) {
t.Fatalf("数据库错误响应泄露底层信息 %q%s", leaked, body)
}
}
}
// TestShopContactPhoneIndexDefinition 验证联系电话索引的类型、唯一性和部分条件。
func TestShopContactPhoneIndexDefinition(t *testing.T) {
env := newShopTestEnv(t)
var indexCount int64
if err := env.db.Raw(`
SELECT COUNT(*)
FROM pg_class
WHERE relname = ?
`, "idx_shop_contact_phone").Scan(&indexCount).Error; err != nil {
t.Fatalf("查询联系电话索引数量失败:%v", err)
}
if os.Getenv("UR60_EXPECT_INDEX_ABSENT") == "1" {
if indexCount != 0 {
t.Fatalf("回滚后联系电话索引仍然存在")
}
return
}
if indexCount != 1 {
t.Fatalf("联系电话索引数量异常:%d", indexCount)
}
var index struct {
AccessMethod string `gorm:"column:access_method"`
IsUnique bool `gorm:"column:is_unique"`
Definition string `gorm:"column:definition"`
Predicate string `gorm:"column:predicate"`
}
err := env.db.Raw(`
SELECT am.amname AS access_method,
ix.indisunique AS is_unique,
pg_get_indexdef(ix.indexrelid) AS definition,
pg_get_expr(ix.indpred, ix.indrelid) AS predicate
FROM pg_index ix
JOIN pg_class i ON i.oid = ix.indexrelid
JOIN pg_am am ON am.oid = i.relam
WHERE i.relname = ?
`, "idx_shop_contact_phone").Scan(&index).Error
if err != nil {
t.Fatalf("查询联系电话索引定义失败:%v", err)
}
if index.AccessMethod != "btree" || index.IsUnique || !strings.Contains(index.Definition, "contact_phone") || !strings.Contains(index.Predicate, "deleted_at IS NULL") {
t.Fatalf("联系电话索引定义不符合契约:%+v", index)
}
}
func newShopTestEnv(t *testing.T) *shopTestEnv {
t.Helper()
if os.Getenv("JUNHONG_DATABASE_HOST") == "" || os.Getenv("JUNHONG_REDIS_ADDRESS") == "" {
t.Skip("未加载 .env.local跳过依赖真实 PostgreSQL 和 Redis 的店铺 HTTP 集成测试")
}
cfg, err := config.Load()
if err != nil {
t.Fatalf("加载测试配置失败:%v", err)
}
logger := zap.NewNop()
db, err := database.InitPostgreSQL(&cfg.Database, logger)
if err != nil {
t.Fatalf("连接 PostgreSQL 失败:%v", err)
}
redisClient, err := database.NewRedisClient(database.RedisConfig{
Address: cfg.Redis.Address + ":" + strconv.Itoa(cfg.Redis.Port),
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.Redis.PoolSize,
MinIdleConns: cfg.Redis.MinIdleConns,
DialTimeout: cfg.Redis.DialTimeout,
ReadTimeout: cfg.Redis.ReadTimeout,
WriteTimeout: cfg.Redis.WriteTimeout,
}, logger)
if err != nil {
t.Fatalf("连接 Redis 失败:%v", err)
}
t.Cleanup(func() {
_ = redisClient.Close()
if sqlDB, dbErr := db.DB(); dbErr == nil {
_ = sqlDB.Close()
}
})
shopStore := postgres.NewShopStore(db, redisClient)
service := shopService.New(shopStore, nil, nil, nil, nil, nil)
shopCommission := shopCommissionService.New(
shopStore,
postgres.NewAccountStore(db, redisClient),
postgres.NewAgentWalletStore(db, redisClient),
postgres.NewCommissionWithdrawalRequestStore(db, redisClient),
postgres.NewCommissionWithdrawalSettingStore(db, redisClient),
postgres.NewCommissionRecordStore(db, redisClient),
postgres.NewAgentWalletTransactionStore(db, redisClient),
db,
logger,
)
tokenManager := auth.NewTokenManager(redisClient, cfg.JWT.AccessTokenTTL, cfg.JWT.RefreshTokenTTL)
authMiddleware := pkgMiddleware.Auth(pkgMiddleware.AuthConfig{
TokenValidator: func(token string) (*pkgMiddleware.UserContextInfo, error) {
info, validateErr := tokenManager.ValidateAccessToken(context.Background(), token)
if validateErr != nil {
return nil, errors.New(errors.CodeInvalidToken, "认证令牌无效或已过期")
}
return &pkgMiddleware.UserContextInfo{
UserID: info.UserID,
UserType: info.UserType,
Username: info.Username,
ShopID: info.ShopID,
EnterpriseID: info.EnterpriseID,
}, nil
},
ShopStore: shopStore,
})
app := fiber.New(fiber.Config{
JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal,
ErrorHandler: internalMiddleware.ErrorHandler(logger),
})
RegisterAdminRoutes(app.Group("/api/admin"), &bootstrap.Handlers{
Shop: admin.NewShopHandler(service, validator.New()),
ShopRole: admin.NewShopRoleHandler(service),
ShopCommission: admin.NewShopCommissionHandler(shopCommission),
}, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin")
return &shopTestEnv{app: app, db: db, redis: redisClient, tokenManager: tokenManager}
}
func (e *shopTestEnv) createShop(t *testing.T, name, phone string, parentID *uint, level int, createdAt time.Time) *model.Shop {
t.Helper()
shop := &model.Shop{
ShopName: name,
ShopCode: fmt.Sprintf("UR60-%d", time.Now().UnixNano()),
ParentID: parentID,
Level: level,
ContactPhone: phone,
Status: constants.ShopStatusEnabled,
}
shop.CreatedAt = createdAt
shop.UpdatedAt = createdAt
shop.Creator = uniqueTestID()
shop.Updater = shop.Creator
if err := e.db.Create(shop).Error; err != nil {
t.Fatalf("创建测试店铺失败:%v", err)
}
t.Cleanup(func() { _ = e.db.Unscoped().Delete(shop).Error })
return shop
}
func (e *shopTestEnv) newToken(t *testing.T, info auth.TokenInfo) string {
t.Helper()
accessToken, refreshToken, err := e.tokenManager.GenerateTokenPair(context.Background(), &info)
if err != nil {
t.Fatalf("创建测试令牌失败:%v", err)
}
t.Cleanup(func() {
_ = e.tokenManager.RevokeToken(context.Background(), accessToken)
_ = e.tokenManager.RevokeToken(context.Background(), refreshToken)
_ = e.redis.Del(context.Background(), constants.RedisUserTokensKey(info.UserID)).Err()
})
return accessToken
}
func (e *shopTestEnv) request(t *testing.T, method, path, token string) (int, []byte) {
t.Helper()
request, err := http.NewRequest(method, path, nil)
if err != nil {
t.Fatalf("创建 HTTP 请求失败:%v", err)
}
if token != "" {
request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token)
}
response, err := e.app.Test(request, -1)
if err != nil {
t.Fatalf("执行 HTTP 请求失败:%v", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatalf("读取 HTTP 响应失败:%v", err)
}
return response.StatusCode, body
}
func uniqueTestID() uint {
return uint(time.Now().UnixNano() % 1_000_000_000)
}