406 lines
21 KiB
Go
406 lines
21 KiB
Go
package routes
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"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"
|
||
apphandler "github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||
internalMiddleware "github.com/break/junhong_cmp_fiber/internal/middleware"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
assetQuery "github.com/break/junhong_cmp_fiber/internal/query/asset"
|
||
packageExpiryQuery "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
|
||
assetService "github.com/break/junhong_cmp_fiber/internal/service/asset"
|
||
customerBinding "github.com/break/junhong_cmp_fiber/internal/service/customer_binding"
|
||
deviceService "github.com/break/junhong_cmp_fiber/internal/service/device"
|
||
iotCardService "github.com/break/junhong_cmp_fiber/internal/service/iot_card"
|
||
"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"
|
||
)
|
||
|
||
// TestAssetResolveHTTPIntegratesAuthenticationPermissionsAndExchangeTrace 验证真实认证、权限、资产解析和双向换货投影。
|
||
func TestAssetResolveHTTPIntegratesAuthenticationPermissionsAndExchangeTrace(t *testing.T) {
|
||
env := newAssetTraceHTTPEnv(t)
|
||
visibleShop := env.createShop(t, "UR86 可见店铺", nil, 1)
|
||
hiddenShop := env.createShop(t, "UR86 不可见店铺", nil, 1)
|
||
oldCard := env.createCard(t, 1, &hiddenShop.ID)
|
||
middleCard := env.createCard(t, 2, &visibleShop.ID)
|
||
nextCard := env.createCard(t, 3, &visibleShop.ID)
|
||
env.createOrder(t, "UR86-HTTP-PREV", oldCard, middleCard, "HTTP 历史前代快照", "HTTP 中间快照", time.Now().Add(-time.Minute))
|
||
env.createOrder(t, "UR86-HTTP-NEXT", middleCard, nextCard, "HTTP 中间快照", "HTTP 历史后代快照", time.Now())
|
||
token := env.newAgentToken(t, visibleShop.ID)
|
||
|
||
status, body := env.request(t, "/api/admin/assets/resolve/"+middleCard.ICCID, token)
|
||
if status != http.StatusOK {
|
||
t.Fatalf("查询中间资产失败,状态 %d:%s", status, body)
|
||
}
|
||
var success struct {
|
||
Code int `json:"code"`
|
||
Data struct {
|
||
AssetID uint `json:"asset_id"`
|
||
ExchangeTrace struct {
|
||
PreviousAsset *struct {
|
||
AssetID *uint `json:"asset_id"`
|
||
Identifier string `json:"identifier"`
|
||
ExchangeNo string `json:"exchange_no"`
|
||
CanView bool `json:"can_view"`
|
||
} `json:"previous_asset"`
|
||
NextAsset *struct {
|
||
AssetID *uint `json:"asset_id"`
|
||
Identifier string `json:"identifier"`
|
||
ExchangeNo string `json:"exchange_no"`
|
||
CanView bool `json:"can_view"`
|
||
} `json:"next_asset"`
|
||
} `json:"exchange_trace"`
|
||
} `json:"data"`
|
||
}
|
||
if err := sonic.Unmarshal(body, &success); err != nil {
|
||
t.Fatalf("解析资产详情响应失败:%v", err)
|
||
}
|
||
if success.Code != errors.CodeSuccess || success.Data.AssetID != middleCard.ID || success.Data.ExchangeTrace.PreviousAsset == nil || success.Data.ExchangeTrace.NextAsset == nil {
|
||
t.Fatalf("双向换货响应不完整:%s", body)
|
||
}
|
||
previous := success.Data.ExchangeTrace.PreviousAsset
|
||
if previous.CanView || previous.AssetID != nil || previous.Identifier != "HTTP 历史前代快照" || previous.ExchangeNo != "UR86-HTTP-PREV" {
|
||
t.Fatalf("不可见前代未按契约隐藏 ID:%s", body)
|
||
}
|
||
next := success.Data.ExchangeTrace.NextAsset
|
||
if !next.CanView || next.AssetID == nil || *next.AssetID != nextCard.ID || next.Identifier != "HTTP 历史后代快照" || next.ExchangeNo != "UR86-HTTP-NEXT" {
|
||
t.Fatalf("可见后代未按契约返回:%s", body)
|
||
}
|
||
|
||
noTraceCard := env.createCard(t, 4, &visibleShop.ID)
|
||
status, body = env.request(t, "/api/admin/assets/resolve/"+noTraceCard.ICCID, token)
|
||
assertHTTPTraceDirections(t, status, body, false, false)
|
||
|
||
previousOnlyOld := env.createCard(t, 5, &visibleShop.ID)
|
||
previousOnlyNew := env.createCard(t, 6, &visibleShop.ID)
|
||
env.createOrder(t, "UR86-HTTP-PREV-ONLY", previousOnlyOld, previousOnlyNew, "仅前代旧快照", "仅前代新快照", time.Now())
|
||
status, body = env.request(t, "/api/admin/assets/resolve/"+previousOnlyNew.ICCID, token)
|
||
assertHTTPTraceDirections(t, status, body, true, false)
|
||
|
||
nextOnlyOld := env.createCard(t, 7, &visibleShop.ID)
|
||
nextOnlyNew := env.createCard(t, 8, &visibleShop.ID)
|
||
env.createOrder(t, "UR86-HTTP-NEXT-ONLY", nextOnlyOld, nextOnlyNew, "仅后代旧快照", "仅后代新快照", time.Now())
|
||
status, body = env.request(t, "/api/admin/assets/resolve/"+nextOnlyOld.ICCID, token)
|
||
assertHTTPTraceDirections(t, status, body, false, true)
|
||
|
||
status, body = env.request(t, "/api/admin/assets/resolve/"+oldCard.ICCID, token)
|
||
if status != http.StatusNotFound {
|
||
t.Fatalf("当前资产无权限时应维持防枚举响应,状态 %d:%s", status, body)
|
||
}
|
||
var denied struct {
|
||
Code int `json:"code"`
|
||
Data any `json:"data"`
|
||
}
|
||
if err := sonic.Unmarshal(body, &denied); err != nil || denied.Code != errors.CodeNotFound || denied.Data != nil {
|
||
t.Fatalf("当前资产无权限响应不符合原契约:%s", body)
|
||
}
|
||
}
|
||
|
||
// TestAssetResolveHTTPReturnsPackageExpiryEstimate 验证后台详情通过真实 JWT、Redis、Fiber 与 PostgreSQL 返回预计最终到期字段。
|
||
func TestAssetResolveHTTPReturnsPackageExpiryEstimate(t *testing.T) {
|
||
env := newAssetTraceHTTPEnv(t)
|
||
shop := env.createShop(t, "UR46详情店铺", nil, 1)
|
||
card := env.createCard(t, 46, &shop.ID)
|
||
now := time.Now().In(time.FixedZone("Asia/Shanghai", 8*60*60))
|
||
expiresAt := now.AddDate(0, 0, 5)
|
||
usage := &model.PackageUsage{OrderID: card.ID, OrderNo: "UR46-HTTP", PackageID: card.ID, UsageType: constants.AssetTypeIotCard, IotCardID: card.ID, DataLimitMB: 1, Status: constants.PackageUsageStatusActive, Priority: 1, ActivatedAt: &now, ExpiresAt: &expiresAt, ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation, CalendarTypeSnapshot: constants.PackageCalendarTypeByDay, DurationDaysSnapshot: 30}
|
||
if err := env.db.Create(usage).Error; err != nil {
|
||
t.Fatalf("创建详情套餐使用记录失败:%v", err)
|
||
}
|
||
status, body := env.request(t, "/api/admin/assets/resolve/"+card.ICCID, env.newAgentToken(t, shop.ID))
|
||
if status != http.StatusOK {
|
||
t.Fatalf("查询资产详情失败:%d %s", status, body)
|
||
}
|
||
var response struct {
|
||
Data struct {
|
||
ExpiresAt *time.Time `json:"estimated_final_expires_at"`
|
||
Days *int `json:"days_until_final_expiry"`
|
||
Status string `json:"expiry_estimate_status"`
|
||
Name string `json:"expiry_estimate_status_name"`
|
||
Expiring bool `json:"is_expiring"`
|
||
} `json:"data"`
|
||
}
|
||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||
t.Fatalf("解析预计到期响应失败:%v", err)
|
||
}
|
||
if response.Data.ExpiresAt == nil || response.Data.Days == nil || response.Data.Status != constants.PackageExpiryEstimateStatusExact || response.Data.Name == "" || !response.Data.Expiring {
|
||
t.Fatalf("预计到期字段不完整:%s", body)
|
||
}
|
||
}
|
||
|
||
// TestClientAssetInfoHTTPReturnsPackageExpiryEstimate 验证 C 端经真实 JWT、Redis 和 Fiber 返回与后台一致的预计到期字段。
|
||
func TestClientAssetInfoHTTPReturnsPackageExpiryEstimate(t *testing.T) {
|
||
env := newAssetTraceHTTPEnv(t)
|
||
card := env.createCard(t, 47, nil)
|
||
now := time.Now().In(time.FixedZone("Asia/Shanghai", 8*60*60))
|
||
expiresAt := now.AddDate(0, 0, 5)
|
||
usage := &model.PackageUsage{OrderID: card.ID, OrderNo: "UR46-CLIENT", PackageID: card.ID, UsageType: constants.AssetTypeIotCard, IotCardID: card.ID, DataLimitMB: 1, Status: constants.PackageUsageStatusActive, Priority: 1, ActivatedAt: &now, ExpiresAt: &expiresAt, ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation, CalendarTypeSnapshot: constants.PackageCalendarTypeByDay, DurationDaysSnapshot: 30}
|
||
if err := env.db.Create(usage).Error; err != nil {
|
||
t.Fatalf("创建 C 端套餐使用记录失败:%v", err)
|
||
}
|
||
customer := &model.PersonalCustomer{WxOpenID: "ur46-client-openid-" + strconv.FormatInt(time.Now().UnixNano(), 10), WxUnionID: "ur46-client-union", Status: constants.StatusEnabled}
|
||
if err := env.db.Create(customer).Error; err != nil {
|
||
t.Fatalf("创建 C 端客户失败:%v", err)
|
||
}
|
||
if err := env.db.Create(&model.PersonalCustomerDevice{CustomerID: customer.ID, VirtualNo: card.VirtualNo, BindAt: now, Status: constants.StatusEnabled}).Error; err != nil {
|
||
t.Fatalf("创建 C 端资产绑定失败:%v", err)
|
||
}
|
||
jwtManager := auth.NewJWTManager("ur46-client-test-secret", time.Hour)
|
||
token, err := jwtManager.GeneratePersonalCustomerToken(customer.ID, "13800000000", constants.AssetTypeIotCard, card.ID)
|
||
if err != nil {
|
||
t.Fatalf("生成 C 端 JWT 失败:%v", err)
|
||
}
|
||
if err := env.redis.Set(context.Background(), constants.RedisPersonalCustomerTokenKey(customer.ID), token, time.Hour).Err(); err != nil {
|
||
t.Fatalf("写入 C 端 Redis Token 失败:%v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
_ = env.redis.Del(context.Background(), constants.RedisPersonalCustomerTokenKey(customer.ID)).Err()
|
||
})
|
||
cardStore := postgres.NewIotCardStore(env.db, env.redis)
|
||
deviceStore := postgres.NewDeviceStore(env.db, env.redis)
|
||
clientHandler := apphandler.NewClientAssetHandler(env.assetService, customerBinding.New(env.db, cardStore, deviceStore), postgres.NewAssetWalletStore(env.db, env.redis), postgres.NewPackageStore(env.db), postgres.NewShopPackageAllocationStore(env.db), cardStore, deviceStore, env.db, zap.NewNop())
|
||
app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(zap.NewNop())})
|
||
personalAuth := internalMiddleware.NewPersonalAuthMiddleware(jwtManager, env.redis, zap.NewNop())
|
||
app.Get("/api/c/v1/asset/info", personalAuth.Authenticate(), clientHandler.GetAssetInfo)
|
||
request, err := http.NewRequest(http.MethodGet, "/api/c/v1/asset/info?identifier="+card.ICCID, nil)
|
||
if err != nil {
|
||
t.Fatalf("创建 C 端请求失败:%v", err)
|
||
}
|
||
request.Header.Set(fiber.HeaderAuthorization, "Bearer "+token)
|
||
response, err := app.Test(request, -1)
|
||
if err != nil {
|
||
t.Fatalf("执行 C 端请求失败:%v", err)
|
||
}
|
||
defer response.Body.Close()
|
||
body, _ := io.ReadAll(response.Body)
|
||
if response.StatusCode != http.StatusOK || !bytes.Contains(body, []byte(`"expiry_estimate_status":"exact"`)) {
|
||
t.Fatalf("C 端预计到期响应错误:%d %s", response.StatusCode, body)
|
||
}
|
||
}
|
||
|
||
// TestAssetListHTTPReturnsPackageExpiryEstimate 验证卡、设备列表在真实 JWT、Redis、Fiber 下批量返回统一预计到期字段。
|
||
func TestAssetListHTTPReturnsPackageExpiryEstimate(t *testing.T) {
|
||
env := newAssetTraceHTTPEnv(t)
|
||
shopStore := postgres.NewShopStore(env.db, env.redis)
|
||
cardStore := postgres.NewIotCardStore(env.db, env.redis)
|
||
deviceStore := postgres.NewDeviceStore(env.db, env.redis)
|
||
seriesStore := postgres.NewPackageSeriesStore(env.db)
|
||
allocationStore := postgres.NewAssetAllocationRecordStore(env.db, env.redis)
|
||
cardSvc := iotCardService.New(env.db, cardStore, shopStore, allocationStore, postgres.NewShopPackageAllocationStore(env.db), postgres.NewShopSeriesAllocationStore(env.db), seriesStore, nil, zap.NewNop(), nil)
|
||
deviceSvc := deviceService.New(env.db, env.redis, deviceStore, postgres.NewDeviceSimBindingStore(env.db, env.redis), cardStore, shopStore, allocationStore, postgres.NewShopPackageAllocationStore(env.db), postgres.NewShopSeriesAllocationStore(env.db), seriesStore, nil, postgres.NewAssetIdentifierStore(env.db), nil, postgres.NewEnterpriseDeviceAuthorizationStore(env.db, env.redis), postgres.NewEnterpriseStore(env.db, env.redis))
|
||
packageExpiry := packageExpiryQuery.NewQuery(env.db)
|
||
cardSvc.SetPackageExpiryQuery(packageExpiry)
|
||
deviceSvc.SetPackageExpiryQuery(packageExpiry)
|
||
authMiddleware := pkgMiddleware.Auth(pkgMiddleware.AuthConfig{TokenValidator: func(token string) (*pkgMiddleware.UserContextInfo, error) {
|
||
info, err := env.tokenManager.ValidateAccessToken(context.Background(), token)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &pkgMiddleware.UserContextInfo{UserID: info.UserID, UserType: info.UserType, ShopID: info.ShopID}, nil
|
||
}, ShopStore: shopStore})
|
||
env.app.Get("/api/admin/iot-cards/standalone", authMiddleware, admin.NewIotCardHandler(cardSvc).ListStandalone)
|
||
env.app.Get("/api/admin/devices", authMiddleware, admin.NewDeviceHandler(deviceSvc).List)
|
||
card := env.createCard(t, 48, nil)
|
||
card.IsStandalone = true
|
||
if err := env.db.Model(card).Update("is_standalone", true).Error; err != nil {
|
||
t.Fatalf("设置独立卡测试状态失败:%v", err)
|
||
}
|
||
device := &model.Device{VirtualNo: "UR46-LIST-DEVICE-" + strconv.FormatInt(time.Now().UnixNano(), 10), Status: constants.DeviceStatusInStock, AssetStatus: constants.AssetStatusInStock}
|
||
if err := env.db.Create(device).Error; err != nil {
|
||
t.Fatalf("创建设备失败:%v", err)
|
||
}
|
||
now := time.Now()
|
||
expiresAt := now.AddDate(0, 0, 5)
|
||
for _, usage := range []*model.PackageUsage{{OrderID: card.ID, OrderNo: "UR46-LIST-CARD", PackageID: card.ID, UsageType: constants.AssetTypeIotCard, IotCardID: card.ID, DataLimitMB: 1, Status: constants.PackageUsageStatusActive, Priority: 1, ActivatedAt: &now, ExpiresAt: &expiresAt, ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation, CalendarTypeSnapshot: constants.PackageCalendarTypeByDay, DurationDaysSnapshot: 30}, {OrderID: device.ID, OrderNo: "UR46-LIST-DEVICE", PackageID: device.ID, UsageType: constants.AssetTypeDevice, DeviceID: device.ID, DataLimitMB: 1, Status: constants.PackageUsageStatusActive, Priority: 1, ActivatedAt: &now, ExpiresAt: &expiresAt, ExpiryBaseSnapshot: constants.PackageExpiryBaseFromActivation, CalendarTypeSnapshot: constants.PackageCalendarTypeByDay, DurationDaysSnapshot: 30}} {
|
||
if err := env.db.Create(usage).Error; err != nil {
|
||
t.Fatalf("创建列表套餐失败:%v", err)
|
||
}
|
||
}
|
||
token := env.newAgentToken(t, 0)
|
||
for _, path := range []string{"/api/admin/iot-cards/standalone?page=1&page_size=100", "/api/admin/devices?page=1&page_size=100"} {
|
||
status, body := env.request(t, path, token)
|
||
if status != http.StatusOK || !bytes.Contains(body, []byte(`"expiry_estimate_status":"exact"`)) {
|
||
t.Fatalf("列表预计到期响应错误:path=%s status=%d body=%s", path, status, body)
|
||
}
|
||
}
|
||
}
|
||
|
||
func assertHTTPTraceDirections(t *testing.T, status int, body []byte, hasPrevious, hasNext bool) {
|
||
t.Helper()
|
||
if status != http.StatusOK {
|
||
t.Fatalf("资产详情状态错误,实际 %d:%s", status, body)
|
||
}
|
||
var response struct {
|
||
Data struct {
|
||
ExchangeTrace struct {
|
||
PreviousAsset any `json:"previous_asset"`
|
||
NextAsset any `json:"next_asset"`
|
||
} `json:"exchange_trace"`
|
||
} `json:"data"`
|
||
}
|
||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||
t.Fatalf("解析换货方向响应失败:%v", err)
|
||
}
|
||
if (response.Data.ExchangeTrace.PreviousAsset != nil) != hasPrevious || (response.Data.ExchangeTrace.NextAsset != nil) != hasNext {
|
||
t.Fatalf("换货方向空值语义错误:previous=%v next=%v body=%s", hasPrevious, hasNext, body)
|
||
}
|
||
}
|
||
|
||
type assetTraceHTTPEnv struct {
|
||
app *fiber.App
|
||
db *gorm.DB
|
||
redis *redis.Client
|
||
tokenManager *auth.TokenManager
|
||
assetService *assetService.Service
|
||
}
|
||
|
||
func newAssetTraceHTTPEnv(t *testing.T) *assetTraceHTTPEnv {
|
||
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)
|
||
}
|
||
log := zap.NewNop()
|
||
db, err := database.InitPostgreSQL(&cfg.Database, log)
|
||
if err != nil {
|
||
t.Fatalf("连接 PostgreSQL 失败:%v", err)
|
||
}
|
||
tx := db.Begin()
|
||
if tx.Error != nil {
|
||
t.Fatalf("开启资产换货链测试事务失败:%v", tx.Error)
|
||
}
|
||
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,
|
||
}, log)
|
||
if err != nil {
|
||
t.Fatalf("连接 Redis 失败:%v", err)
|
||
}
|
||
t.Cleanup(func() {
|
||
_ = tx.Rollback().Error
|
||
_ = redisClient.Close()
|
||
if sqlDB, dbErr := db.DB(); dbErr == nil {
|
||
_ = sqlDB.Close()
|
||
}
|
||
})
|
||
|
||
shopStore := postgres.NewShopStore(tx, redisClient)
|
||
deviceStore := postgres.NewDeviceStore(tx, redisClient)
|
||
cardStore := postgres.NewIotCardStore(tx, redisClient)
|
||
assetSvc := assetService.New(
|
||
tx, deviceStore, cardStore,
|
||
postgres.NewPackageUsageStore(tx, redisClient), postgres.NewPackageStore(tx), postgres.NewPackageSeriesStore(tx),
|
||
postgres.NewDeviceSimBindingStore(tx, redisClient), shopStore, redisClient, nil, nil,
|
||
postgres.NewAssetIdentifierStore(tx), postgres.NewOrderStore(tx, redisClient), postgres.NewOrderItemStore(tx, redisClient),
|
||
postgres.NewExchangeOrderStore(tx), nil,
|
||
)
|
||
assetSvc.SetPackageExpiryQuery(packageExpiryQuery.NewQuery(tx))
|
||
handler := admin.NewAssetHandler(assetSvc, nil, nil, nil, nil, nil, assetQuery.NewExchangeTraceQuery(tx, log))
|
||
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}, nil
|
||
},
|
||
ShopStore: shopStore,
|
||
})
|
||
app := fiber.New(fiber.Config{JSONEncoder: sonic.Marshal, JSONDecoder: sonic.Unmarshal, ErrorHandler: internalMiddleware.ErrorHandler(log)})
|
||
RegisterAdminRoutes(app.Group("/api/admin"), &bootstrap.Handlers{Asset: handler}, &bootstrap.Middlewares{AdminAuth: authMiddleware}, nil, "/api/admin")
|
||
return &assetTraceHTTPEnv{app: app, db: tx, redis: redisClient, tokenManager: tokenManager, assetService: assetSvc}
|
||
}
|
||
|
||
func (e *assetTraceHTTPEnv) createShop(t *testing.T, name string, parentID *uint, level int) *model.Shop {
|
||
t.Helper()
|
||
shop := &model.Shop{ShopName: name, ShopCode: fmt.Sprintf("UR86-%d", time.Now().UnixNano()), ParentID: parentID, Level: level, Status: constants.ShopStatusEnabled}
|
||
if err := e.db.Create(shop).Error; err != nil {
|
||
t.Fatalf("创建资产换货链测试店铺失败:%v", err)
|
||
}
|
||
return shop
|
||
}
|
||
|
||
func (e *assetTraceHTTPEnv) createCard(t *testing.T, suffix int, shopID *uint) *model.IotCard {
|
||
t.Helper()
|
||
iccid := fmt.Sprintf("8986333333333333%04d", suffix)
|
||
card := &model.IotCard{ICCID: iccid, ICCID19: iccid[:19], VirtualNo: fmt.Sprintf("UR86-HTTP-CARD-%04d", suffix), ShopID: shopID, Status: constants.IotCardStatusInStock, AssetStatus: constants.AssetStatusInStock}
|
||
if err := e.db.Create(card).Error; err != nil {
|
||
t.Fatalf("创建资产换货链 HTTP 测试卡失败:%v", err)
|
||
}
|
||
return card
|
||
}
|
||
|
||
func (e *assetTraceHTTPEnv) createOrder(t *testing.T, exchangeNo string, oldCard, newCard *model.IotCard, oldSnapshot, newSnapshot string, completedAt time.Time) {
|
||
t.Helper()
|
||
newID := newCard.ID
|
||
order := &model.ExchangeOrder{
|
||
ExchangeNo: exchangeNo, FlowType: constants.ExchangeFlowTypeDirect,
|
||
OldAssetType: constants.ExchangeAssetTypeIotCard, OldAssetID: oldCard.ID, OldAssetIdentifier: oldSnapshot,
|
||
NewAssetType: constants.ExchangeAssetTypeIotCard, NewAssetID: &newID, NewAssetIdentifier: newSnapshot,
|
||
ExchangeReason: "UR86 HTTP 测试", Status: constants.ExchangeStatusCompleted, CompletedAt: &completedAt,
|
||
}
|
||
if err := e.db.Create(order).Error; err != nil {
|
||
t.Fatalf("创建资产换货链 HTTP 测试单失败:%v", err)
|
||
}
|
||
}
|
||
|
||
func (e *assetTraceHTTPEnv) newAgentToken(t *testing.T, shopID uint) string {
|
||
t.Helper()
|
||
info := &auth.TokenInfo{UserID: uint(time.Now().UnixNano() % 1_000_000_000), UserType: constants.UserTypeAgent, ShopID: shopID, Username: "UR86测试代理"}
|
||
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 *assetTraceHTTPEnv) request(t *testing.T, path, token string) (int, []byte) {
|
||
t.Helper()
|
||
request, err := http.NewRequest(http.MethodGet, path, nil)
|
||
if err != nil {
|
||
t.Fatalf("创建资产换货链 HTTP 请求失败:%v", err)
|
||
}
|
||
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
|
||
}
|