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 allocationTestEnv struct { db *gorm.DB rdb *redis.Client tokenManager *auth.TokenManager app *fiber.App adminToken string agentToken string adminID uint agentID uint shopID uint subShopID uint t *testing.T } func setupAllocationTestEnv(t *testing.T) *allocationTestEnv { t.Helper() t.Setenv("CONFIG_ENV", "dev") t.Setenv("CONFIG_PATH", "../../configs/config.dev.yaml") 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) db.Exec("DELETE FROM tb_asset_allocation_record WHERE asset_identifier LIKE 'ALLOC_TEST%'") db.Exec("DELETE FROM tb_iot_card WHERE iccid LIKE 'ALLOC_TEST%'") db.Exec("DROP INDEX IF EXISTS uk_asset_allocation_no") 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") shop := &model.Shop{ ShopName: fmt.Sprintf("测试店铺_%d", time.Now().UnixNano()), ShopCode: fmt.Sprintf("ALLOC_SHOP_%d", time.Now().UnixNano()), ContactName: "测试联系人", ContactPhone: "13800000001", Status: 1, } require.NoError(t, db.Create(shop).Error) subShop := &model.Shop{ ShopName: fmt.Sprintf("测试下级店铺_%d", time.Now().UnixNano()), ShopCode: fmt.Sprintf("ALLOC_SUB_%d", time.Now().UnixNano()), ParentID: &shop.ID, Level: 2, ContactName: "下级联系人", ContactPhone: "13800000002", Status: 1, } require.NoError(t, db.Create(subShop).Error) agentAccount := &model.Account{ Username: fmt.Sprintf("agent_alloc_%d", time.Now().UnixNano()), Phone: fmt.Sprintf("139%08d", time.Now().UnixNano()%100000000), Password: "hashed_password", UserType: constants.UserTypeAgent, ShopID: &shop.ID, Status: 1, } require.NoError(t, db.Create(agentAccount).Error) agentToken, _ := testutil.GenerateTestToken(t, rdb, agentAccount, "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 &allocationTestEnv{ db: db, rdb: rdb, tokenManager: tokenManager, app: app, adminToken: adminToken, agentToken: agentToken, adminID: superAdmin.ID, agentID: agentAccount.ID, shopID: shop.ID, subShopID: subShop.ID, t: t, } } func (e *allocationTestEnv) teardown() { e.db.Exec("DELETE FROM tb_iot_card WHERE iccid LIKE 'ALLOC_TEST%'") e.db.Exec("DELETE FROM tb_asset_allocation_record WHERE asset_identifier LIKE 'ALLOC_TEST%'") e.db.Exec("DELETE FROM tb_shop WHERE shop_code LIKE 'ALLOC_%'") e.db.Exec("DELETE FROM tb_account WHERE username LIKE 'agent_alloc_%'") 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() } func TestStandaloneCardAllocation_AllocateByList(t *testing.T) { env := setupAllocationTestEnv(t) defer env.teardown() cards := []*model.IotCard{ {ICCID: "ALLOC_TEST001", CardType: "data_card", CarrierID: 1, Status: constants.IotCardStatusInStock}, {ICCID: "ALLOC_TEST002", CardType: "data_card", CarrierID: 1, Status: constants.IotCardStatusInStock}, {ICCID: "ALLOC_TEST003", CardType: "data_card", CarrierID: 1, Status: constants.IotCardStatusInStock}, } for _, card := range cards { require.NoError(t, env.db.Create(card).Error) } t.Run("平台分配卡给一级店铺", func(t *testing.T) { reqBody := map[string]interface{}{ "to_shop_id": env.shopID, "selection_type": "list", "iccids": []string{"ALLOC_TEST001", "ALLOC_TEST002"}, "remark": "测试分配", } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/iot-cards/standalone/allocate", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+env.adminToken) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) t.Logf("Allocate response: code=%d, message=%s, data=%v", result.Code, result.Message, result.Data) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message) if result.Data != nil { dataMap := result.Data.(map[string]interface{}) assert.Equal(t, float64(2), dataMap["total_count"]) assert.Equal(t, float64(2), dataMap["success_count"]) assert.Equal(t, float64(0), dataMap["fail_count"]) } ctx := pkggorm.SkipDataPermission(context.Background()) var updatedCards []model.IotCard env.db.WithContext(ctx).Where("iccid IN ?", []string{"ALLOC_TEST001", "ALLOC_TEST002"}).Find(&updatedCards) for _, card := range updatedCards { assert.Equal(t, env.shopID, *card.ShopID, "卡应分配给目标店铺") assert.Equal(t, constants.IotCardStatusDistributed, card.Status, "状态应为已分销") } var recordCount int64 env.db.WithContext(ctx).Model(&model.AssetAllocationRecord{}). Where("asset_identifier IN ?", []string{"ALLOC_TEST001", "ALLOC_TEST002"}). Count(&recordCount) assert.Equal(t, int64(2), recordCount, "应创建2条分配记录") }) t.Run("代理分配卡给下级店铺", func(t *testing.T) { reqBody := map[string]interface{}{ "to_shop_id": env.subShopID, "selection_type": "list", "iccids": []string{"ALLOC_TEST001"}, "remark": "代理分配测试", } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/iot-cards/standalone/allocate", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+env.agentToken) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) t.Logf("Agent allocate response: code=%d, message=%s", result.Code, result.Message) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 0, result.Code, "代理应能分配给下级: %s", result.Message) }) t.Run("分配不存在的卡应返回空结果", func(t *testing.T) { reqBody := map[string]interface{}{ "to_shop_id": env.shopID, "selection_type": "list", "iccids": []string{"NOT_EXISTS_001", "NOT_EXISTS_002"}, } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/iot-cards/standalone/allocate", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+env.adminToken) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) if result.Data != nil { dataMap := result.Data.(map[string]interface{}) assert.Equal(t, float64(0), dataMap["total_count"]) } }) } func TestStandaloneCardAllocation_Recall(t *testing.T) { env := setupAllocationTestEnv(t) defer env.teardown() shopID := env.shopID cards := []*model.IotCard{ {ICCID: "ALLOC_TEST101", CardType: "data_card", CarrierID: 1, Status: constants.IotCardStatusDistributed, ShopID: &shopID}, {ICCID: "ALLOC_TEST102", CardType: "data_card", CarrierID: 1, Status: constants.IotCardStatusDistributed, ShopID: &shopID}, } for _, card := range cards { require.NoError(t, env.db.Create(card).Error) } t.Run("平台回收卡", func(t *testing.T) { reqBody := map[string]interface{}{ "from_shop_id": env.shopID, "selection_type": "list", "iccids": []string{"ALLOC_TEST101"}, "remark": "平台回收测试", } bodyBytes, _ := json.Marshal(reqBody) req := httptest.NewRequest("POST", "/api/admin/iot-cards/standalone/recall", bytes.NewReader(bodyBytes)) req.Header.Set("Authorization", "Bearer "+env.adminToken) req.Header.Set("Content-Type", "application/json") resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) t.Logf("Recall response: code=%d, message=%s, data=%v", result.Code, result.Message, result.Data) assert.Equal(t, 200, resp.StatusCode) assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message) ctx := pkggorm.SkipDataPermission(context.Background()) var recalledCard model.IotCard env.db.WithContext(ctx).Where("iccid = ?", "ALLOC_TEST101").First(&recalledCard) assert.Nil(t, recalledCard.ShopID, "平台回收后shop_id应为NULL") assert.Equal(t, constants.IotCardStatusInStock, recalledCard.Status, "状态应恢复为在库") }) } func TestAssetAllocationRecord_List(t *testing.T) { env := setupAllocationTestEnv(t) defer env.teardown() fromShopID := env.shopID records := []*model.AssetAllocationRecord{ { AllocationNo: fmt.Sprintf("AL%d001", time.Now().UnixNano()), AllocationType: constants.AssetAllocationTypeAllocate, AssetType: constants.AssetTypeIotCard, AssetID: 1, AssetIdentifier: "ALLOC_TEST_REC001", FromOwnerType: constants.OwnerTypePlatform, ToOwnerType: constants.OwnerTypeShop, ToOwnerID: env.shopID, OperatorID: env.adminID, }, { AllocationNo: fmt.Sprintf("RC%d001", time.Now().UnixNano()), AllocationType: constants.AssetAllocationTypeRecall, AssetType: constants.AssetTypeIotCard, AssetID: 2, AssetIdentifier: "ALLOC_TEST_REC002", FromOwnerType: constants.OwnerTypeShop, FromOwnerID: &fromShopID, ToOwnerType: constants.OwnerTypePlatform, ToOwnerID: 0, OperatorID: env.adminID, }, } for _, record := range records { require.NoError(t, env.db.Create(record).Error) } t.Run("获取分配记录列表", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/admin/asset-allocation-records?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) }) t.Run("按分配类型过滤", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/admin/asset-allocation-records?allocation_type=allocate", nil) req.Header.Set("Authorization", "Bearer "+env.adminToken) resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) }) t.Run("获取分配记录详情", func(t *testing.T) { url := fmt.Sprintf("/api/admin/asset-allocation-records/%d", records[0].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) }) t.Run("未认证请求应返回错误", func(t *testing.T) { req := httptest.NewRequest("GET", "/api/admin/asset-allocation-records", nil) resp, err := env.app.Test(req, -1) require.NoError(t, err) defer resp.Body.Close() var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.NotEqual(t, 0, result.Code, "未认证请求应返回错误码") }) }