修复店铺列表参数校验与企业权限
This commit is contained in:
@@ -84,7 +84,7 @@ func initHandlers(svc *services, deps *Dependencies) *Handlers {
|
||||
ClientRealname: app.NewClientRealnameHandler(svc.Asset, svc.CustomerBinding, iotCardStore, deviceSimBindingStore, carrierStore, deps.GatewayClient, deps.Logger, svc.PollingManualTrigger),
|
||||
ClientDevice: app.NewClientDeviceHandler(svc.Asset, svc.CustomerBinding, deviceStore, deviceSimBindingStore, iotCardStore, deps.GatewayClient, deps.Logger),
|
||||
ClientRechargeOrder: app.NewClientRechargeOrderHandler(rechargeOrderStore, paymentStore, deps.Logger),
|
||||
Shop: admin.NewShopHandler(svc.Shop),
|
||||
Shop: admin.NewShopHandler(svc.Shop, validate),
|
||||
ShopRole: admin.NewShopRoleHandler(svc.Shop),
|
||||
AdminAuth: admin.NewAuthHandler(svc.Auth, validate),
|
||||
ShopCommission: admin.NewShopCommissionHandler(svc.ShopCommission),
|
||||
|
||||
@@ -3,27 +3,51 @@ package admin
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
shopService "github.com/break/junhong_cmp_fiber/internal/service/shop"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
)
|
||||
|
||||
// ShopHandler 店铺管理处理器。
|
||||
type ShopHandler struct {
|
||||
service *shopService.Service
|
||||
service *shopService.Service
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func NewShopHandler(service *shopService.Service) *ShopHandler {
|
||||
return &ShopHandler{service: service}
|
||||
// NewShopHandler 创建店铺管理处理器。
|
||||
func NewShopHandler(service *shopService.Service, validate *validator.Validate) *ShopHandler {
|
||||
return &ShopHandler{service: service, validator: validate}
|
||||
}
|
||||
|
||||
// List 查询店铺列表。
|
||||
// GET /api/admin/shops
|
||||
func (h *ShopHandler) List(c *fiber.Ctx) error {
|
||||
var req dto.ShopListRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
|
||||
h.logListValidationFailure(c, err)
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
if hasExplicitZeroPagination(c, &req) {
|
||||
err := errors.New(errors.CodeInvalidParam)
|
||||
h.logListValidationFailure(c, err)
|
||||
return err
|
||||
}
|
||||
if h.validator == nil {
|
||||
return errors.New(errors.CodeInternalError, "店铺列表校验器未配置")
|
||||
}
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
h.logListValidationFailure(c, err)
|
||||
return errors.New(errors.CodeInvalidParam)
|
||||
}
|
||||
|
||||
normalizeShopListPagination(&req)
|
||||
|
||||
shops, total, err := h.service.ListShopResponses(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
@@ -33,6 +57,31 @@ func (h *ShopHandler) List(c *fiber.Ctx) error {
|
||||
return response.SuccessWithPagination(c, shops, total, req.Page, req.PageSize)
|
||||
}
|
||||
|
||||
func hasExplicitZeroPagination(c *fiber.Ctx, req *dto.ShopListRequest) bool {
|
||||
queryArgs := c.Context().QueryArgs()
|
||||
return queryArgs.Has("page") && req.Page == 0 || queryArgs.Has("page_size") && req.PageSize == 0
|
||||
}
|
||||
|
||||
func (h *ShopHandler) logListValidationFailure(c *fiber.Ctx, err error) {
|
||||
logger.GetAppLogger().Warn("店铺列表参数验证失败",
|
||||
zap.String("method", c.Method()),
|
||||
zap.String("path", c.Path()),
|
||||
zap.String("query", c.Context().QueryArgs().String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
|
||||
func normalizeShopListPagination(req *dto.ShopListRequest) {
|
||||
if req.Page == 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = constants.DefaultPageSize
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建店铺。
|
||||
// POST /api/admin/shops
|
||||
func (h *ShopHandler) Create(c *fiber.Ctx) error {
|
||||
var req dto.CreateShopRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
@@ -47,6 +96,8 @@ func (h *ShopHandler) Create(c *fiber.Ctx) error {
|
||||
return response.Success(c, shop)
|
||||
}
|
||||
|
||||
// Update 更新店铺。
|
||||
// PUT /api/admin/shops/:id
|
||||
func (h *ShopHandler) Update(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -66,6 +117,8 @@ func (h *ShopHandler) Update(c *fiber.Ctx) error {
|
||||
return response.Success(c, shop)
|
||||
}
|
||||
|
||||
// Delete 删除店铺。
|
||||
// DELETE /api/admin/shops/:id
|
||||
func (h *ShopHandler) Delete(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/handler/admin"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||
)
|
||||
|
||||
@@ -12,47 +15,61 @@ func registerShopRoutes(router fiber.Router, handler *admin.ShopHandler, doc *op
|
||||
shops := router.Group("/shops")
|
||||
groupPath := basePath + "/shops"
|
||||
|
||||
Register(shops, doc, groupPath, "GET", "", handler.List, RouteSpec{
|
||||
Summary: "店铺列表",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.ShopListRequest),
|
||||
Output: new(dto.ShopPageResult),
|
||||
Auth: true,
|
||||
Register(shops, doc, groupPath, "GET", "", coreShopManagement(handler.List), RouteSpec{
|
||||
Summary: "店铺列表",
|
||||
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。分页默认第 1 页、每页 20 条;page 最小为 1,page_size 范围为 1 至 100。所有筛选条件在查询前统一校验。",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.ShopListRequest),
|
||||
Output: new(dto.ShopPageResult),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(shops, doc, groupPath, "POST", "", handler.Create, RouteSpec{
|
||||
Summary: "创建店铺",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.CreateShopRequest),
|
||||
Output: new(dto.ShopResponse),
|
||||
Auth: true,
|
||||
Register(shops, doc, groupPath, "POST", "", coreShopManagement(handler.Create), RouteSpec{
|
||||
Summary: "创建店铺",
|
||||
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.CreateShopRequest),
|
||||
Output: new(dto.ShopResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(shops, doc, groupPath, "PUT", "/:id", handler.Update, RouteSpec{
|
||||
Summary: "更新店铺",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.UpdateShopParams),
|
||||
Output: new(dto.ShopResponse),
|
||||
Auth: true,
|
||||
Register(shops, doc, groupPath, "PUT", "/:id", coreShopManagement(handler.Update), RouteSpec{
|
||||
Summary: "更新店铺",
|
||||
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.UpdateShopParams),
|
||||
Output: new(dto.ShopResponse),
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(shops, doc, groupPath, "DELETE", "/:id", handler.Delete, RouteSpec{
|
||||
Summary: "删除店铺",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
Register(shops, doc, groupPath, "DELETE", "/:id", coreShopManagement(handler.Delete), RouteSpec{
|
||||
Summary: "删除店铺",
|
||||
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.IDReq),
|
||||
Output: nil,
|
||||
Auth: true,
|
||||
})
|
||||
|
||||
Register(shops, doc, groupPath, "GET", "/cascade", handler.Cascade, RouteSpec{
|
||||
Summary: "店铺联级查询",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.ShopCascadeRequest),
|
||||
Output: new([]dto.ShopCascadeItem),
|
||||
Auth: true,
|
||||
Register(shops, doc, groupPath, "GET", "/cascade", coreShopManagement(handler.Cascade), RouteSpec{
|
||||
Summary: "店铺联级查询",
|
||||
Description: "仅超级管理员、平台账号和代理账号可访问,企业账号返回 403。",
|
||||
Tags: []string{"店铺管理"},
|
||||
Input: new(dto.ShopCascadeRequest),
|
||||
Output: new([]dto.ShopCascadeItem),
|
||||
Auth: true,
|
||||
})
|
||||
}
|
||||
|
||||
func coreShopManagement(handler fiber.Handler) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
if middleware.GetUserTypeFromContext(c.UserContext()) == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "无权限访问店铺管理功能")
|
||||
}
|
||||
return handler(c)
|
||||
}
|
||||
}
|
||||
|
||||
func registerShopRoleRoutes(router fiber.Router, handler *admin.ShopRoleHandler, doc *openapi.Generator, basePath string) {
|
||||
shops := router.Group("/shops")
|
||||
groupPath := basePath + "/shops"
|
||||
|
||||
357
internal/routes/shop_integration_test.go
Normal file
357
internal/routes/shop_integration_test.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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"
|
||||
shopService "github.com/break/junhong_cmp_fiber/internal/service/shop"
|
||||
"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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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: 1, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 != "无权限访问店铺管理功能" || response.Data != nil {
|
||||
t.Fatalf("企业账号禁止响应不符合契约:%s", body)
|
||||
}
|
||||
if !strings.Contains(string(body), "timestamp") {
|
||||
t.Fatalf("企业账号禁止响应缺少时间戳:%s", body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreShopRestrictionDoesNotAffectOtherShopRoutes(t *testing.T) {
|
||||
env := newShopTestEnv(t)
|
||||
token := env.newToken(t, auth.TokenInfo{
|
||||
UserID: uniqueTestID(),
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
EnterpriseID: uniqueTestID(),
|
||||
Username: "ur60-enterprise",
|
||||
})
|
||||
|
||||
status, body := env.request(t, http.MethodGet, "/api/admin/shops/1/roles", token)
|
||||
if status == http.StatusForbidden && strings.Contains(string(body), "无权限访问店铺管理功能") {
|
||||
t.Fatalf("核心店铺管理拦截误伤店铺角色路由:%s", body)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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),
|
||||
}, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin")
|
||||
|
||||
return &shopTestEnv{app: app, db: db, redis: redisClient, tokenManager: tokenManager}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user