移除所有测试代码和测试要求
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m33s
**变更说明**: - 删除所有 *_test.go 文件(单元测试、集成测试、验收测试、流程测试) - 删除整个 tests/ 目录 - 更新 CLAUDE.md:用"测试禁令"章节替换所有测试要求 - 删除测试生成 Skill (openspec-generate-acceptance-tests) - 删除测试生成命令 (opsx:gen-tests) - 更新 tasks.md:删除所有测试相关任务 **新规范**: - ❌ 禁止编写任何形式的自动化测试 - ❌ 禁止创建 *_test.go 文件 - ❌ 禁止在任务中包含测试相关工作 - ✅ 仅当用户明确要求时才编写测试 **原因**: 业务系统的正确性通过人工验证和生产环境监控保证,测试代码维护成本高于价值。 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,322 +0,0 @@
|
||||
# 验收测试 (Acceptance Tests)
|
||||
|
||||
验收测试验证单个 API 的契约:给定输入,期望输出。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. **来源于 Spec**:每个测试用例对应 Spec 中的一个 Scenario
|
||||
2. **测试先于实现**:在功能实现前生成,预期全部 FAIL
|
||||
3. **契约验证**:验证 API 的输入输出契约,不测试内部实现
|
||||
4. **必须有破坏点**:每个测试必须注释说明什么代码变更会导致失败
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
tests/acceptance/
|
||||
├── README.md # 本文件
|
||||
├── account_acceptance_test.go # 账号管理验收测试
|
||||
├── package_acceptance_test.go # 套餐管理验收测试
|
||||
├── shop_package_acceptance_test.go # 店铺套餐分配验收测试
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 测试模板
|
||||
|
||||
```go
|
||||
package acceptance
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 验收测试:{功能名称}
|
||||
// 来源:openspec/changes/{change-name}/specs/{capability}/spec.md
|
||||
// ============================================================
|
||||
|
||||
func Test{Capability}_Acceptance(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: {场景名称}
|
||||
// GIVEN: {前置条件}
|
||||
// WHEN: {触发动作}
|
||||
// THEN: {预期结果}
|
||||
// AND: {额外验证}
|
||||
//
|
||||
// 破坏点:{描述什么代码变更会导致此测试失败}
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_{场景名称}", func(t *testing.T) {
|
||||
// GIVEN: 设置前置条件
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
// WHEN: 执行操作
|
||||
body := map[string]interface{}{
|
||||
// 请求体
|
||||
}
|
||||
resp, err := client.Request("POST", "/api/admin/xxx", body)
|
||||
require.NoError(t, err)
|
||||
|
||||
// THEN: 验证结果
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = resp.JSON(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, int(result["code"].(float64)))
|
||||
|
||||
// AND: 额外验证(如数据库状态)
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 测试分类
|
||||
|
||||
### 正常场景 (Happy Path)
|
||||
|
||||
```go
|
||||
t.Run("Scenario_成功创建资源", func(t *testing.T) {
|
||||
// 测试正常流程
|
||||
})
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
|
||||
```go
|
||||
t.Run("Scenario_参数缺失返回400", func(t *testing.T) {
|
||||
// 测试缺少必填参数
|
||||
})
|
||||
|
||||
t.Run("Scenario_参数格式错误返回400", func(t *testing.T) {
|
||||
// 测试参数格式不符合要求
|
||||
})
|
||||
```
|
||||
|
||||
### 权限校验
|
||||
|
||||
```go
|
||||
t.Run("Scenario_无权限返回403", func(t *testing.T) {
|
||||
// 测试权限不足的情况
|
||||
})
|
||||
|
||||
t.Run("Scenario_跨店铺访问返回403", func(t *testing.T) {
|
||||
// 测试越权访问
|
||||
})
|
||||
```
|
||||
|
||||
### 业务规则
|
||||
|
||||
```go
|
||||
t.Run("Scenario_重复创建返回409", func(t *testing.T) {
|
||||
// 测试业务规则冲突
|
||||
})
|
||||
|
||||
t.Run("Scenario_删除已使用资源返回400", func(t *testing.T) {
|
||||
// 测试业务规则限制
|
||||
})
|
||||
```
|
||||
|
||||
## 破坏点注释规范
|
||||
|
||||
每个测试必须包含"破坏点"注释,说明什么代码变更会导致测试失败:
|
||||
|
||||
```go
|
||||
// 破坏点:如果删除 handler.Create 中的 store.Create 调用,此测试将失败
|
||||
// 破坏点:如果移除参数校验中的 name 必填检查,此测试将失败
|
||||
// 破坏点:如果响应不包含创建的资源 ID,此测试将失败
|
||||
// 破坏点:如果删除权限检查中间件,此测试将失败
|
||||
```
|
||||
|
||||
**为什么需要破坏点**:
|
||||
1. 证明测试真正验证了功能
|
||||
2. 帮助理解测试意图
|
||||
3. 重构时快速定位影响
|
||||
|
||||
## Table-Driven 模式
|
||||
|
||||
对于同一 API 的多个场景,使用 table-driven 模式:
|
||||
|
||||
```go
|
||||
func TestPackage_Create_Validation(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body map[string]interface{}
|
||||
expectedStatus int
|
||||
expectedCode int
|
||||
breakpoint string
|
||||
}{
|
||||
{
|
||||
name: "名称为空",
|
||||
body: map[string]interface{}{
|
||||
"name": "",
|
||||
"price": 9900,
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedCode: 4000, // CodeInvalidParam
|
||||
breakpoint: "移除 name 必填校验",
|
||||
},
|
||||
{
|
||||
name: "价格为负",
|
||||
body: map[string]interface{}{
|
||||
"name": "测试套餐",
|
||||
"price": -100,
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedCode: 4000,
|
||||
breakpoint: "移除 price >= 0 校验",
|
||||
},
|
||||
{
|
||||
name: "时长为0",
|
||||
body: map[string]interface{}{
|
||||
"name": "测试套餐",
|
||||
"price": 9900,
|
||||
"duration": 0,
|
||||
},
|
||||
expectedStatus: 400,
|
||||
expectedCode: 4000,
|
||||
breakpoint: "移除 duration > 0 校验",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run("Scenario_"+tt.name, func(t *testing.T) {
|
||||
// 破坏点: {tt.breakpoint}
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
resp, err := client.Request("POST", "/api/admin/packages", tt.body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expectedStatus, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = resp.JSON(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedCode, int(result["code"].(float64)))
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有验收测试
|
||||
source .env.local && go test -v ./tests/acceptance/...
|
||||
|
||||
# 运行特定功能的验收测试
|
||||
source .env.local && go test -v ./tests/acceptance/... -run TestPackage
|
||||
|
||||
# 运行特定场景
|
||||
source .env.local && go test -v ./tests/acceptance/... -run "Scenario_成功创建"
|
||||
```
|
||||
|
||||
## 测试环境
|
||||
|
||||
验收测试使用 `IntegrationTestEnv`,提供:
|
||||
|
||||
- **事务隔离**:每个测试在独立事务中运行,自动回滚
|
||||
- **Redis 清理**:测试前自动清理相关 Redis 键
|
||||
- **身份切换**:支持不同角色的请求
|
||||
|
||||
```go
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// 以超级管理员身份请求
|
||||
env.AsSuperAdmin().Request("GET", "/api/admin/xxx", nil)
|
||||
|
||||
// 以平台用户身份请求
|
||||
env.AsPlatformUser(accountID).Request("GET", "/api/admin/xxx", nil)
|
||||
|
||||
// 以代理商身份请求
|
||||
env.AsShopAgent(shopID).Request("GET", "/api/admin/xxx", nil)
|
||||
|
||||
// 以企业用户身份请求
|
||||
env.AsEnterprise(enterpriseID).Request("GET", "/api/admin/xxx", nil)
|
||||
```
|
||||
|
||||
## 与 Spec 的对应关系
|
||||
|
||||
```markdown
|
||||
# Spec 中的 Scenario
|
||||
|
||||
#### Scenario: 成功创建套餐
|
||||
- **GIVEN** 用户已登录且有创建权限
|
||||
- **WHEN** POST /api/admin/packages with valid data
|
||||
- **THEN** 返回 201 和套餐详情
|
||||
- **AND** 数据库中存在该套餐记录
|
||||
```
|
||||
|
||||
对应测试:
|
||||
|
||||
```go
|
||||
// 直接从 Spec Scenario 转换
|
||||
t.Run("Scenario_成功创建套餐", func(t *testing.T) {
|
||||
// GIVEN: 用户已登录且有创建权限
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
// WHEN: POST /api/admin/packages with valid data
|
||||
resp, err := client.Request("POST", "/api/admin/packages", validBody)
|
||||
|
||||
// THEN: 返回 201 和套餐详情
|
||||
assert.Equal(t, 201, resp.StatusCode)
|
||||
|
||||
// AND: 数据库中存在该套餐记录
|
||||
// 验证数据库状态
|
||||
})
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 验收测试和集成测试的区别?
|
||||
|
||||
| 方面 | 验收测试 | 集成测试 |
|
||||
|------|---------|---------|
|
||||
| 来源 | Spec Scenario | 开发者编写 |
|
||||
| 目的 | 验证 API 契约 | 验证系统集成 |
|
||||
| 粒度 | 单 API | 可能涉及多 API |
|
||||
| 时机 | 实现前生成 | 实现后编写 |
|
||||
|
||||
### Q: 测试 PASS 了但功能还没实现?
|
||||
|
||||
说明测试写得太弱。检查:
|
||||
1. 是否验证了响应状态码
|
||||
2. 是否验证了响应体结构
|
||||
3. 是否验证了数据库状态变化
|
||||
4. 破坏点是否准确
|
||||
|
||||
### Q: 如何处理需要前置数据的测试?
|
||||
|
||||
在 GIVEN 阶段创建必要的前置数据:
|
||||
|
||||
```go
|
||||
t.Run("Scenario_删除已分配的套餐失败", func(t *testing.T) {
|
||||
// GIVEN: 存在一个已分配给店铺的套餐
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
// 创建套餐
|
||||
createResp, _ := client.Request("POST", "/api/admin/packages", packageBody)
|
||||
var createResult map[string]interface{}
|
||||
createResp.JSON(&createResult)
|
||||
packageID := uint(createResult["data"].(map[string]interface{})["id"].(float64))
|
||||
|
||||
// 分配给店铺
|
||||
client.Request("POST", "/api/admin/shop-packages", map[string]interface{}{
|
||||
"package_id": packageID,
|
||||
"shop_id": 1,
|
||||
})
|
||||
|
||||
// WHEN: 尝试删除套餐
|
||||
resp, _ := client.Request("DELETE", fmt.Sprintf("/api/admin/packages/%d", packageID), nil)
|
||||
|
||||
// THEN: 返回 400
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
```
|
||||
@@ -1,444 +0,0 @@
|
||||
package acceptance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 验收测试:佣金计算重构
|
||||
// 来源:openspec/changes/refactor-one-time-commission-allocation/specs/commission-calculation/spec.md
|
||||
// 来源:openspec/changes/refactor-one-time-commission-allocation/specs/commission-trigger/spec.md
|
||||
// ============================================================
|
||||
|
||||
func TestCommissionCalculation_SeriesAllocationQuery_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createCommissionTestSeries(t, env, "佣金测试系列")
|
||||
|
||||
createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
|
||||
createSeriesAllocationForCommission(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 直接查询系列分配
|
||||
// GIVEN: 存在 shop_id + series_id 的系列分配记录
|
||||
// WHEN: 通过 shop_id 和 series_id 查询
|
||||
// THEN: 返回唯一匹配的记录,包含 one_time_commission_amount
|
||||
//
|
||||
// 破坏点:如果查询 API 不支持 series_id 筛选,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_直接查询系列分配", func(t *testing.T) {
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d",
|
||||
childShop.ID, series.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
require.Len(t, items, 1, "应返回唯一匹配记录")
|
||||
|
||||
allocation := items[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(5000), allocation["one_time_commission_amount"],
|
||||
"佣金金额应为 5000 分")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 系列分配不存在
|
||||
// GIVEN: shop_id + series_id 组合不存在分配记录
|
||||
// WHEN: 查询该组合
|
||||
// THEN: 返回空列表
|
||||
//
|
||||
// 破坏点:如果查询不正确处理空结果,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_系列分配不存在", func(t *testing.T) {
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=99999",
|
||||
childShop.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
list := data["items"].([]interface{})
|
||||
assert.Empty(t, list, "不存在的组合应返回空列表")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionCalculation_EnableOneTimeCommission_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
series := createCommissionTestSeriesWithConfig(t, env, "启用佣金系列", true)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 检查系列是否启用一次性佣金
|
||||
// GIVEN: 系列配置 enable_one_time_commission = true
|
||||
// WHEN: 查询系列详情
|
||||
// THEN: 响应包含 enable_one_time_commission = true
|
||||
//
|
||||
// 破坏点:如果系列 API 不返回 enable_one_time_commission 字段,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_检查系列是否启用一次性佣金", func(t *testing.T) {
|
||||
path := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
enableOneTime, ok := data["enable_one_time_commission"]
|
||||
assert.True(t, ok, "响应应包含 enable_one_time_commission 字段")
|
||||
assert.Equal(t, true, enableOneTime, "应为 true")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 批量查询启用一次性佣金的系列
|
||||
// GIVEN: 存在多个系列,部分启用一次性佣金
|
||||
// WHEN: 查询系列列表并按 enable_one_time_commission 筛选
|
||||
// THEN: 返回符合条件的系列
|
||||
//
|
||||
// 破坏点:如果不支持 enable_one_time_commission 筛选,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_批量查询启用一次性佣金的系列", func(t *testing.T) {
|
||||
createCommissionTestSeriesWithConfig(t, env, "禁用佣金系列", false)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("GET", "/api/admin/package-series?enable_one_time_commission=true", nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
list := data["items"].([]interface{})
|
||||
|
||||
for _, item := range list {
|
||||
seriesItem := item.(map[string]interface{})
|
||||
enableVal, hasField := seriesItem["enable_one_time_commission"]
|
||||
if hasField {
|
||||
assert.Equal(t, true, enableVal, "筛选结果应全部为启用状态")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionCalculation_ChainAllocation_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
level1Shop := env.CreateTestShop("一级代理", 1, nil)
|
||||
level2Shop := env.CreateTestShop("二级代理", 2, &level1Shop.ID)
|
||||
level3Shop := env.CreateTestShop("三级代理", 3, &level2Shop.ID)
|
||||
series := createCommissionTestSeries(t, env, "链式分配系列")
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 链式分配金额计算
|
||||
// GIVEN:
|
||||
// - 平台给一级:one_time_commission_amount = 10000(100元)
|
||||
// - 一级给二级:one_time_commission_amount = 8000(80元)
|
||||
// - 二级给三级:one_time_commission_amount = 5000(50元)
|
||||
// WHEN: 查询各级的系列分配
|
||||
// THEN: 各级金额正确
|
||||
//
|
||||
// 破坏点:如果分配金额不正确保存,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_链式分配金额计算", func(t *testing.T) {
|
||||
createPlatformSeriesAllocationForCommission(t, env, level1Shop.ID, series.ID, 10000)
|
||||
createSeriesAllocationForCommission(t, env, level1Shop.ID, level2Shop.ID, series.ID, 8000)
|
||||
createSeriesAllocationForCommission(t, env, level2Shop.ID, level3Shop.ID, series.ID, 5000)
|
||||
|
||||
verifyAllocationAmount(t, env, level1Shop.ID, series.ID, 10000)
|
||||
verifyAllocationAmount(t, env, level2Shop.ID, series.ID, 8000)
|
||||
verifyAllocationAmount(t, env, level3Shop.ID, series.ID, 5000)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 单级代理
|
||||
// GIVEN: 一级代理直接销售(无下级)
|
||||
// WHEN: 查询一级的系列分配
|
||||
// THEN: 一级获得完整的 one_time_commission_amount
|
||||
//
|
||||
// 破坏点:如果单级分配不生效,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_单级代理", func(t *testing.T) {
|
||||
singleShop := env.CreateTestShop("单级代理", 1, nil)
|
||||
singleSeries := createCommissionTestSeries(t, env, "单级系列")
|
||||
|
||||
createPlatformSeriesAllocationForCommission(t, env, singleShop.ID, singleSeries.ID, 10000)
|
||||
verifyAllocationAmount(t, env, singleShop.ID, singleSeries.ID, 10000)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionCalculation_TriggerConfig_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createCommissionTestSeries(t, env, "触发配置系列")
|
||||
|
||||
createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 累计达到阈值触发佣金配置
|
||||
// GIVEN: 系列分配设置为累计充值触发,阈值 1000 元
|
||||
// WHEN: 创建系列分配
|
||||
// THEN: 配置正确保存
|
||||
//
|
||||
// 破坏点:如果触发配置不保存,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_累计达到阈值触发佣金配置", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
"enable_one_time_commission": true,
|
||||
"one_time_commission_trigger": "accumulated_recharge",
|
||||
"one_time_commission_threshold": 100000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["enable_one_time_commission"])
|
||||
assert.Equal(t, "accumulated_recharge", data["one_time_commission_trigger"])
|
||||
assert.Equal(t, float64(100000), data["one_time_commission_threshold"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 首次充值触发配置
|
||||
// GIVEN: 系列分配设置为首次充值触发,阈值 100 元
|
||||
// WHEN: 创建系列分配
|
||||
// THEN: 配置正确保存
|
||||
//
|
||||
// 破坏点:如果 first_recharge 触发类型不支持,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_首次充值触发配置", func(t *testing.T) {
|
||||
newChildShop := env.CreateTestShop("首充测试店铺", 2, &parentShop.ID)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newChildShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
"enable_one_time_commission": true,
|
||||
"one_time_commission_trigger": "first_recharge",
|
||||
"one_time_commission_threshold": 10000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "first_recharge", data["one_time_commission_trigger"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionStats_Allocation_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
series := createCommissionTestSeries(t, env, "统计测试系列")
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 创建佣金统计记录关联系列分配
|
||||
// GIVEN: 存在系列分配记录
|
||||
// WHEN: 查询佣金统计
|
||||
// THEN: 统计记录的 allocation_id 指向 ShopSeriesAllocation.id
|
||||
//
|
||||
// 破坏点:如果统计不关联系列分配,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_佣金统计关联系列分配", func(t *testing.T) {
|
||||
allocation := createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000)
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-commission-stats?shop_id=%d&series_id=%d",
|
||||
parentShop.ID, series.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
if result.Code == 0 && result.Data != nil {
|
||||
data := result.Data.(map[string]interface{})
|
||||
if list, ok := data["items"].([]interface{}); ok && len(list) > 0 {
|
||||
stats := list[0].(map[string]interface{})
|
||||
if allocationID, exists := stats["allocation_id"]; exists {
|
||||
assert.Equal(t, float64(allocation.ID), allocationID,
|
||||
"统计应关联到系列分配 ID")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
func createCommissionTestSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err, "创建测试系列失败")
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createCommissionTestSeriesWithConfig(t *testing.T, env *integ.IntegrationTestEnv, name string, enableOneTime bool) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
EnableOneTimeCommission: enableOneTime,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err, "创建测试系列失败")
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createPlatformSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shopID,
|
||||
SeriesID: seriesID,
|
||||
AllocatorShopID: 0,
|
||||
OneTimeCommissionAmount: amount,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建平台系列分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
|
||||
func createSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, allocatorShopID, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shopID,
|
||||
SeriesID: seriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
OneTimeCommissionAmount: amount,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建系列分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
|
||||
func verifyAllocationAmount(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, expectedAmount int64) {
|
||||
t.Helper()
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", shopID, seriesID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
list := data["items"].([]interface{})
|
||||
require.NotEmpty(t, list, "应存在分配记录")
|
||||
|
||||
allocation := list[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(expectedAmount), allocation["one_time_commission_amount"],
|
||||
"店铺 %d 系列 %d 的佣金金额应为 %d", shopID, seriesID, expectedAmount)
|
||||
}
|
||||
@@ -1,847 +0,0 @@
|
||||
package acceptance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 验收测试:套餐系列分配 (ShopSeriesAllocation)
|
||||
// 来源:openspec/changes/refactor-one-time-commission-allocation/specs/shop-series-allocation/spec.md
|
||||
// ============================================================
|
||||
|
||||
func TestShopSeriesAllocation_Create_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 准备测试数据:创建店铺层级和套餐系列
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createTestPackageSeries(t, env, "测试系列")
|
||||
|
||||
// 先为一级代理创建系列分配(平台分配)
|
||||
platformAllocation := createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 成功分配套餐系列
|
||||
// GIVEN: 代理已有该系列的分配权限
|
||||
// WHEN: POST /api/admin/shop-series-allocations 设置 one_time_commission_amount = 5000
|
||||
// THEN: 返回 200 和分配记录详情
|
||||
// AND: 下级代理的一次性佣金上限为 50 元
|
||||
//
|
||||
// 破坏点:如果 Handler 不调用 Service.Create,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_成功分配套餐系列", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000, // 50 元
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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, "应返回成功: %s", result.Message)
|
||||
|
||||
// 验证响应包含 one_time_commission_amount
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok, "响应 data 应为对象")
|
||||
assert.Equal(t, float64(5000), data["one_time_commission_amount"], "一次性佣金金额应为 5000 分")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 下级金额不能超过上级
|
||||
// GIVEN: 上级分配金额为 10000 分(100 元)
|
||||
// WHEN: 尝试为下级分配 15000 分(150 元)
|
||||
// THEN: 返回 400 错误 "一次性佣金金额不能超过您的分配上限"
|
||||
//
|
||||
// 破坏点:如果移除金额上限校验,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_下级金额不能超过上级", func(t *testing.T) {
|
||||
newChildShop := env.CreateTestShop("新下级店铺", 2, &parentShop.ID)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newChildShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 15000, // 超过上级的 10000
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "应返回错误")
|
||||
assert.Contains(t, result.Message, "超过", "错误消息应包含'超过'")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 分配时启用一次性佣金和强充
|
||||
// GIVEN: 代理有分配权限
|
||||
// WHEN: POST 创建分配,启用一次性佣金(累计充值触发,阈值 1000 元),启用强充(100 元)
|
||||
// THEN: 系统保存完整配置
|
||||
//
|
||||
// 破坏点:如果不保存 enable_one_time_commission 等字段,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_分配时启用一次性佣金和强充", func(t *testing.T) {
|
||||
newChildShop := env.CreateTestShop("新下级店铺2", 2, &parentShop.ID)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newChildShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
"enable_one_time_commission": true,
|
||||
"one_time_commission_trigger": "accumulated_recharge",
|
||||
"one_time_commission_threshold": 100000, // 1000 元
|
||||
"enable_force_recharge": true,
|
||||
"force_recharge_amount": 10000, // 100 元
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["enable_one_time_commission"])
|
||||
assert.Equal(t, "accumulated_recharge", data["one_time_commission_trigger"])
|
||||
assert.Equal(t, float64(100000), data["one_time_commission_threshold"])
|
||||
assert.Equal(t, true, data["enable_force_recharge"])
|
||||
assert.Equal(t, float64(10000), data["force_recharge_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 尝试分配未拥有的系列
|
||||
// GIVEN: 代理没有某系列的分配权限
|
||||
// WHEN: 尝试为下级分配该系列
|
||||
// THEN: 返回 403/400 "您没有该套餐系列的分配权限"
|
||||
//
|
||||
// 破坏点:如果不检查代理是否拥有系列权限,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_尝试分配未拥有的系列", func(t *testing.T) {
|
||||
unownedSeries := createTestPackageSeries(t, env, "未分配系列")
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"series_id": unownedSeries.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 应返回 400 或 403
|
||||
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403,
|
||||
"应返回 400 或 403,实际: %d", resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 尝试分配给非直属下级
|
||||
// GIVEN: 店铺 A 是一级,店铺 B 是二级(A 的下级),店铺 C 是三级(B 的下级)
|
||||
// WHEN: 店铺 A 尝试直接分配给店铺 C
|
||||
// THEN: 返回 403 "只能为直属下级分配套餐"
|
||||
//
|
||||
// 破坏点:如果不检查是否为直属下级,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_尝试分配给非直属下级", func(t *testing.T) {
|
||||
grandChildShop := env.CreateTestShop("三级代理", 3, &childShop.ID)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": grandChildShop.ID, // 非直属下级
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 重复分配同一系列
|
||||
// GIVEN: 已为下级店铺分配了某系列
|
||||
// WHEN: 再次尝试分配同一系列
|
||||
// THEN: 返回 409 "该店铺已分配此套餐系列"
|
||||
//
|
||||
// 破坏点:如果不检查唯一索引,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_重复分配同一系列", func(t *testing.T) {
|
||||
newChildShop := env.CreateTestShop("重复测试店铺", 2, &parentShop.ID)
|
||||
|
||||
// 第一次分配
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newChildShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// 第二次分配(应失败)
|
||||
resp2, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
assert.True(t, resp2.StatusCode == 400 || resp2.StatusCode == 409,
|
||||
"重复分配应返回 400 或 409,实际: %d", resp2.StatusCode)
|
||||
})
|
||||
|
||||
_ = platformAllocation // 使用变量避免编译警告
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocation_List_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createTestPackageSeries(t, env, "列表测试系列")
|
||||
|
||||
// 创建分配记录
|
||||
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 查询所有分配
|
||||
// GIVEN: 存在多条分配记录
|
||||
// WHEN: GET /api/admin/shop-series-allocations 不带筛选条件
|
||||
// THEN: 返回该代理创建的所有分配记录
|
||||
// AND: 每条记录包含 one_time_commission_amount 字段
|
||||
//
|
||||
// 破坏点:如果 List API 不返回数据,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_查询所有分配", func(t *testing.T) {
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("GET", "/api/admin/shop-series-allocations", nil)
|
||||
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)
|
||||
|
||||
// 验证返回列表格式
|
||||
data := result.Data.(map[string]interface{})
|
||||
items, ok := data["items"].([]interface{})
|
||||
require.True(t, ok, "响应应包含 items 字段")
|
||||
require.NotEmpty(t, items, "列表不应为空")
|
||||
|
||||
// 验证第一条记录包含 one_time_commission_amount
|
||||
firstItem := items[0].(map[string]interface{})
|
||||
_, hasAmount := firstItem["one_time_commission_amount"]
|
||||
assert.True(t, hasAmount, "记录应包含 one_time_commission_amount 字段")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 按店铺筛选
|
||||
// GIVEN: 存在多个店铺的分配记录
|
||||
// WHEN: GET /api/admin/shop-series-allocations?shop_id=xxx
|
||||
// THEN: 只返回该店铺的分配记录
|
||||
//
|
||||
// 破坏点:如果不支持 shop_id 筛选,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_按店铺筛选", func(t *testing.T) {
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d", childShop.ID)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("GET", path, nil)
|
||||
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)
|
||||
|
||||
// 验证筛选结果
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
for _, item := range items {
|
||||
record := item.(map[string]interface{})
|
||||
assert.Equal(t, float64(childShop.ID), record["shop_id"],
|
||||
"筛选结果应只包含指定店铺")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocation_Update_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createTestPackageSeries(t, env, "更新测试系列")
|
||||
|
||||
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 更新一次性佣金金额
|
||||
// GIVEN: 存在一条分配记录,金额为 5000
|
||||
// WHEN: PUT /api/admin/shop-series-allocations/:id 将金额改为 6000
|
||||
// THEN: 更新成功,返回更新后的记录
|
||||
//
|
||||
// 破坏点:如果 Update API 不保存金额变更,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_更新一次性佣金金额", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"one_time_commission_amount": 6000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("PUT", path, jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(6000), data["one_time_commission_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 更新金额不能超过上级上限
|
||||
// GIVEN: 上级分配金额上限为 10000
|
||||
// WHEN: 尝试将金额更新为 15000
|
||||
// THEN: 返回 400 错误
|
||||
//
|
||||
// 破坏点:如果更新时不检查金额上限,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_更新金额不能超过上级上限", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"one_time_commission_amount": 15000, // 超过上级的 10000
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("PUT", path, jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 更新强充配置
|
||||
// GIVEN: 分配记录存在
|
||||
// WHEN: PUT 启用强充,设置金额 100 元
|
||||
// THEN: 配置更新成功
|
||||
//
|
||||
// 破坏点:如果不保存强充配置,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_更新强充配置", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"enable_force_recharge": true,
|
||||
"force_recharge_amount": 10000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("PUT", path, jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["enable_force_recharge"])
|
||||
assert.Equal(t, float64(10000), data["force_recharge_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 更新不存在的分配
|
||||
// GIVEN: 分配 ID 不存在
|
||||
// WHEN: PUT /api/admin/shop-series-allocations/99999
|
||||
// THEN: 返回 404 "分配记录不存在"
|
||||
//
|
||||
// 破坏点:如果不检查记录是否存在,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_更新不存在的分配", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"one_time_commission_amount": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("PUT", "/api/admin/shop-series-allocations/99999", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocation_Delete_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createTestPackageSeries(t, env, "删除测试系列")
|
||||
|
||||
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 删除系列分配时检查套餐分配
|
||||
// GIVEN: 系列分配存在,且有依赖的套餐分配
|
||||
// WHEN: DELETE /api/admin/shop-series-allocations/:id
|
||||
// THEN: 返回 400 "存在关联的套餐分配,无法删除"
|
||||
//
|
||||
// 破坏点:如果不检查套餐分配依赖,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_删除系列分配时检查套餐分配", func(t *testing.T) {
|
||||
// 创建系列分配
|
||||
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
|
||||
|
||||
// 创建依赖的套餐分配
|
||||
pkg := createTestPackage(t, env, series.ID, "测试套餐")
|
||||
createPackageAllocationWithSeriesAllocation(t, env, childShop.ID, pkg.ID, allocation.ID)
|
||||
|
||||
// 尝试删除系列分配
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("DELETE", path, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "关联", "错误消息应提及关联")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 成功删除无依赖的系列分配
|
||||
// GIVEN: 系列分配存在,无套餐分配依赖
|
||||
// WHEN: DELETE /api/admin/shop-series-allocations/:id
|
||||
// THEN: 删除成功
|
||||
//
|
||||
// 破坏点:如果 Delete API 不工作,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_成功删除无依赖的系列分配", func(t *testing.T) {
|
||||
newChildShop := env.CreateTestShop("新下级", 2, &parentShop.ID)
|
||||
allocation := createSeriesAllocationDirectly(t, env, parentShop.ID, newChildShop.ID, series.ID, 5000)
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||
resp, err := env.AsUser(createTestAgentAccount(t, env, parentShop.ID)).
|
||||
Request("DELETE", path, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopSeriesAllocation_Platform_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
series := createTestPackageSeries(t, env, "平台分配测试系列")
|
||||
// 设置系列的一次性佣金上限(假设固定 150 元)
|
||||
setSeriesOneTimeCommissionLimit(t, env, series.ID, 15000)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 平台为一级代理分配
|
||||
// GIVEN: 平台管理员
|
||||
// WHEN: POST 为一级代理分配套餐系列,设置 one_time_commission_amount = 10000
|
||||
// THEN: 分配成功
|
||||
//
|
||||
// 破坏点:如果平台无法创建分配,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_平台为一级代理分配", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": parentShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 10000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 平台可自由设定金额
|
||||
// GIVEN: 平台管理员
|
||||
// WHEN: 平台为一级代理分配任意金额(如 20000)
|
||||
// THEN: 分配成功(平台无上限限制)
|
||||
//
|
||||
// 破坏点:如果平台分配被上限限制,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_平台可自由设定金额", func(t *testing.T) {
|
||||
newShop := env.CreateTestShop("新一级代理", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 20000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 平台配置强充要求
|
||||
// GIVEN: 平台管理员
|
||||
// WHEN: POST 为一级代理分配系列,启用强充,force_recharge_amount = 10000
|
||||
// THEN: 配置保存成功
|
||||
//
|
||||
// 破坏点:如果不保存强充配置,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_平台配置强充要求", func(t *testing.T) {
|
||||
newShop := env.CreateTestShop("强充测试店铺", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": newShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 10000,
|
||||
"enable_force_recharge": true,
|
||||
"force_recharge_amount": 10000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["enable_force_recharge"])
|
||||
assert.Equal(t, float64(10000), data["force_recharge_amount"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopPackageAllocation_SeriesDependency_Acceptance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("一级代理", 1, nil)
|
||||
childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID)
|
||||
series := createTestPackageSeries(t, env, "依赖测试系列")
|
||||
pkg := createTestPackage(t, env, series.ID, "依赖测试套餐")
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 未分配系列时分配套餐失败
|
||||
// GIVEN: 下级店铺未被分配系列 X
|
||||
// WHEN: 代理尝试为下级分配套餐 A(属于系列 X)
|
||||
// THEN: 返回 400 "请先分配该套餐所属的系列"
|
||||
//
|
||||
// 破坏点:如果不检查系列分配依赖,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_未分配系列时分配套餐失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"package_id": pkg.ID,
|
||||
"cost_price": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-package-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "系列", "错误消息应提及系列")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 先分配系列再分配套餐
|
||||
// GIVEN: 下级店铺已被分配系列 X
|
||||
// WHEN: 代理为下级分配套餐 A(属于系列 X)
|
||||
// THEN: 分配成功,套餐分配关联到系列分配记录
|
||||
//
|
||||
// 破坏点:如果不关联 series_allocation_id,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_先分配系列再分配套餐", func(t *testing.T) {
|
||||
// 先分配系列
|
||||
createPlatformSeriesAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
seriesAllocation := createSeriesAllocationDirectly(t, env, parentShop.ID, childShop.ID, series.ID, 5000)
|
||||
|
||||
// 再分配套餐
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"package_id": pkg.ID,
|
||||
"cost_price": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("POST", "/api/admin/shop-package-allocations", jsonBody)
|
||||
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)
|
||||
|
||||
// 验证关联
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(seriesAllocation.ID), data["series_allocation_id"],
|
||||
"套餐分配应关联到系列分配")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Scenario: 套餐分配只包含成本价
|
||||
// GIVEN: 套餐分配 API
|
||||
// WHEN: 创建或查询套餐分配
|
||||
// THEN: 请求/响应只包含 cost_price,不包含 one_time_commission_amount
|
||||
//
|
||||
// 破坏点:如果响应包含 one_time_commission_amount,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Scenario_套餐分配只包含成本价", func(t *testing.T) {
|
||||
// 查询已创建的套餐分配
|
||||
resp, err := env.AsSuperAdmin().
|
||||
Request("GET", fmt.Sprintf("/api/admin/shop-package-allocations?shop_id=%d", childShop.ID), nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
if len(items) > 0 {
|
||||
firstItem := items[0].(map[string]interface{})
|
||||
_, hasCostPrice := firstItem["cost_price"]
|
||||
_, hasOneTimeCommission := firstItem["one_time_commission_amount"]
|
||||
|
||||
assert.True(t, hasCostPrice, "应包含 cost_price")
|
||||
assert.False(t, hasOneTimeCommission, "不应包含 one_time_commission_amount")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
func createTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err, "创建测试套餐系列失败")
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createTestPackage(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, name string) *model.Package {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
pkg := &model.Package{
|
||||
PackageCode: fmt.Sprintf("PKG_%d", timestamp),
|
||||
PackageName: name,
|
||||
SeriesID: seriesID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
CostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(pkg).Error
|
||||
require.NoError(t, err, "创建测试套餐失败")
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func createTestAgentAccount(t *testing.T, env *integ.IntegrationTestEnv, shopID uint) *model.Account {
|
||||
t.Helper()
|
||||
return env.CreateTestAccount("agent", "password123", constants.UserTypeAgent, &shopID, nil)
|
||||
}
|
||||
|
||||
// createPlatformSeriesAllocation 模拟平台为一级代理创建的系列分配
|
||||
// 注意:由于 ShopSeriesAllocation 模型可能尚未创建,这里直接通过数据库操作模拟
|
||||
func createPlatformSeriesAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shopID,
|
||||
SeriesID: seriesID,
|
||||
AllocatorShopID: 0, // 平台分配
|
||||
OneTimeCommissionAmount: amount,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建平台系列分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
|
||||
// createSeriesAllocationDirectly 直接在数据库创建系列分配记录
|
||||
func createSeriesAllocationDirectly(t *testing.T, env *integ.IntegrationTestEnv, allocatorShopID, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shopID,
|
||||
SeriesID: seriesID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
OneTimeCommissionAmount: amount,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建系列分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
|
||||
// createPackageAllocationWithSeriesAllocation 创建关联系列分配的套餐分配
|
||||
func createPackageAllocationWithSeriesAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, packageID, seriesAllocationID uint) *model.ShopPackageAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: shopID,
|
||||
PackageID: packageID,
|
||||
SeriesAllocationID: &seriesAllocationID,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建套餐分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
|
||||
// setSeriesOneTimeCommissionLimit 设置系列的一次性佣金上限(假设在 PackageSeries 或配置中)
|
||||
func setSeriesOneTimeCommissionLimit(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, limit int64) {
|
||||
t.Helper()
|
||||
|
||||
// 更新系列配置
|
||||
err := env.TX.Model(&model.PackageSeries{}).Where("id = ?", seriesID).Updates(map[string]interface{}{
|
||||
"enable_one_time_commission": true,
|
||||
// 假设有 one_time_commission_config 字段存储配置
|
||||
}).Error
|
||||
require.NoError(t, err, "设置系列佣金上限失败")
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
# 业务流程测试 (Flow Tests)
|
||||
|
||||
流程测试验证多个 API 组合的完整业务场景,确保端到端流程正确。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. **来源于 Spec Business Flow**:每个测试对应 Spec 中的一个 Business Flow
|
||||
2. **跨 API 验证**:验证多个 API 调用的组合行为
|
||||
3. **状态共享**:流程中的数据(如 ID)在 steps 之间传递
|
||||
4. **角色切换**:不同 step 可能由不同角色执行
|
||||
5. **必须有破坏点和依赖声明**
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
tests/flows/
|
||||
├── README.md # 本文件
|
||||
├── package_lifecycle_flow_test.go # 套餐完整生命周期
|
||||
├── order_purchase_flow_test.go # 订单购买流程
|
||||
├── commission_settlement_flow_test.go # 佣金结算流程
|
||||
├── iot_card_import_activate_flow_test.go # IoT 卡导入激活流程
|
||||
└── ...
|
||||
```
|
||||
|
||||
## 测试模板
|
||||
|
||||
```go
|
||||
package flows
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 流程测试:{流程名称}
|
||||
// 来源:openspec/changes/{change-name}/specs/{capability}/spec.md
|
||||
// 参与者:{角色1}, {角色2}, ...
|
||||
// ============================================================
|
||||
|
||||
func TestFlow_{FlowName}(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// ========================================================
|
||||
// 流程级共享状态
|
||||
// 在 steps 之间传递的数据
|
||||
// ========================================================
|
||||
var (
|
||||
resourceID uint
|
||||
orderID uint
|
||||
// 其他需要共享的状态...
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 1: {步骤名称}
|
||||
// 角色: {执行角色}
|
||||
// 调用: {HTTP Method} {Path}
|
||||
// 预期: {预期结果}
|
||||
//
|
||||
// 依赖: 无(首个步骤)
|
||||
// 破坏点:{描述什么代码变更会导致此测试失败}
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step1_{步骤名称}", func(t *testing.T) {
|
||||
client := env.AsSuperAdmin() // 或其他角色
|
||||
|
||||
body := map[string]interface{}{
|
||||
// 请求体
|
||||
}
|
||||
resp, err := client.Request("POST", "/api/admin/xxx", body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = resp.JSON(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 提取共享状态
|
||||
data := result["data"].(map[string]interface{})
|
||||
resourceID = uint(data["id"].(float64))
|
||||
require.NotZero(t, resourceID, "资源 ID 不能为空")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 2: {步骤名称}
|
||||
// 角色: {执行角色}
|
||||
// 调用: {HTTP Method} {Path}
|
||||
// 预期: {预期结果}
|
||||
//
|
||||
// 依赖: Step 1 的 resourceID
|
||||
// 破坏点:{描述什么代码变更会导致此测试失败}
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step2_{步骤名称}", func(t *testing.T) {
|
||||
if resourceID == 0 {
|
||||
t.Skip("依赖 Step 1 创建的 resourceID")
|
||||
}
|
||||
|
||||
client := env.AsShopAgent(1) // 切换到代理商角色
|
||||
|
||||
resp, err := client.Request("GET", fmt.Sprintf("/api/admin/xxx/%d", resourceID), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
// 验证和提取数据...
|
||||
})
|
||||
|
||||
// 更多 steps...
|
||||
}
|
||||
```
|
||||
|
||||
## 流程测试 vs 验收测试
|
||||
|
||||
| 方面 | 验收测试 | 流程测试 |
|
||||
|------|---------|---------|
|
||||
| 来源 | Spec Scenario | Spec Business Flow |
|
||||
| 粒度 | 单 API | 多 API 组合 |
|
||||
| 状态 | 独立 | steps 之间共享 |
|
||||
| 角色 | 通常单一 | 可能多角色 |
|
||||
| 目的 | 验证 API 契约 | 验证业务场景 |
|
||||
|
||||
## 状态共享模式
|
||||
|
||||
### 使用包级变量
|
||||
|
||||
```go
|
||||
func TestFlow_PackageLifecycle(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// 流程级共享状态
|
||||
var (
|
||||
packageID uint
|
||||
allocationID uint
|
||||
orderID uint
|
||||
)
|
||||
|
||||
t.Run("Step1_创建套餐", func(t *testing.T) {
|
||||
// ... 创建套餐
|
||||
packageID = extractedID
|
||||
})
|
||||
|
||||
t.Run("Step2_分配套餐", func(t *testing.T) {
|
||||
if packageID == 0 {
|
||||
t.Skip("依赖 Step 1")
|
||||
}
|
||||
// 使用 packageID
|
||||
allocationID = extractedID
|
||||
})
|
||||
|
||||
t.Run("Step3_创建订单", func(t *testing.T) {
|
||||
if allocationID == 0 {
|
||||
t.Skip("依赖 Step 2")
|
||||
}
|
||||
// 使用 allocationID
|
||||
orderID = extractedID
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 依赖声明规范
|
||||
|
||||
每个 step 必须声明依赖:
|
||||
|
||||
```go
|
||||
// ------------------------------------------------------------
|
||||
// Step 3: 代理商查看可售套餐
|
||||
// 角色: 代理商
|
||||
// 调用: GET /api/admin/shop-packages
|
||||
// 预期: 列表包含刚分配的套餐
|
||||
//
|
||||
// 依赖: Step 1 的 packageID, Step 2 的分配操作
|
||||
// 破坏点:如果查询不按 shop_id 过滤,代理商会看到其他店铺的套餐
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step3_代理商查看可售套餐", func(t *testing.T) {
|
||||
if packageID == 0 {
|
||||
t.Skip("依赖 Step 1 创建的 packageID")
|
||||
}
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## 多角色流程
|
||||
|
||||
```go
|
||||
func TestFlow_CrossRoleWorkflow(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
var (
|
||||
resourceID uint
|
||||
shopID uint = 1
|
||||
)
|
||||
|
||||
// Step 1: 平台管理员创建资源
|
||||
t.Run("Step1_平台创建资源", func(t *testing.T) {
|
||||
client := env.AsSuperAdmin()
|
||||
// ...
|
||||
resourceID = extractedID
|
||||
})
|
||||
|
||||
// Step 2: 平台管理员分配给代理商
|
||||
t.Run("Step2_分配给代理商", func(t *testing.T) {
|
||||
client := env.AsSuperAdmin()
|
||||
// ...
|
||||
})
|
||||
|
||||
// Step 3: 代理商查看资源(角色切换!)
|
||||
t.Run("Step3_代理商查看", func(t *testing.T) {
|
||||
client := env.AsShopAgent(shopID) // 切换到代理商
|
||||
// ...
|
||||
})
|
||||
|
||||
// Step 4: 代理商创建订单
|
||||
t.Run("Step4_代理商创建订单", func(t *testing.T) {
|
||||
client := env.AsShopAgent(shopID)
|
||||
// ...
|
||||
})
|
||||
|
||||
// Step 5: 平台管理员查看统计(再次切换)
|
||||
t.Run("Step5_平台查看统计", func(t *testing.T) {
|
||||
client := env.AsSuperAdmin()
|
||||
// ...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 破坏点注释规范
|
||||
|
||||
流程测试的破坏点更侧重于跨 API 的影响:
|
||||
|
||||
```go
|
||||
// 破坏点:如果套餐创建 API 不返回 ID,后续步骤无法执行
|
||||
// 破坏点:如果分配 API 不检查套餐是否存在,可能分配无效套餐
|
||||
// 破坏点:如果代理商查询不过滤 shop_id,会看到其他店铺的数据
|
||||
// 破坏点:如果订单创建不验证套餐有效期,可能购买过期套餐
|
||||
// 破坏点:如果佣金计算不在事务中,可能导致数据不一致
|
||||
```
|
||||
|
||||
## 异常流程测试
|
||||
|
||||
流程测试也应覆盖异常场景:
|
||||
|
||||
```go
|
||||
func TestFlow_PackageLifecycle_Exceptions(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 异常流程:尝试删除已分配的套餐
|
||||
// 预期:删除失败,返回业务错误
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Exception_删除已分配套餐", func(t *testing.T) {
|
||||
// Step 1: 创建套餐
|
||||
// Step 2: 分配给店铺
|
||||
// Step 3: 尝试删除(预期失败)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 异常流程:代理商访问其他店铺的套餐
|
||||
// 预期:访问被拒绝
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Exception_跨店铺访问", func(t *testing.T) {
|
||||
// Step 1: 平台创建并分配给店铺 A
|
||||
// Step 2: 店铺 B 的代理商尝试访问(预期 403)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有流程测试
|
||||
source .env.local && go test -v ./tests/flows/...
|
||||
|
||||
# 运行特定流程
|
||||
source .env.local && go test -v ./tests/flows/... -run TestFlow_PackageLifecycle
|
||||
|
||||
# 运行特定步骤
|
||||
source .env.local && go test -v ./tests/flows/... -run "Step3"
|
||||
```
|
||||
|
||||
## 与 Spec Business Flow 的对应关系
|
||||
|
||||
```markdown
|
||||
# Spec 中的 Business Flow
|
||||
|
||||
### Flow: 套餐完整生命周期
|
||||
|
||||
**参与者**: 平台管理员, 代理商
|
||||
|
||||
**流程步骤**:
|
||||
|
||||
1. **创建套餐**
|
||||
- 角色: 平台管理员
|
||||
- 调用: POST /api/admin/packages
|
||||
- 预期: 返回套餐 ID
|
||||
|
||||
2. **分配给代理商**
|
||||
- 角色: 平台管理员
|
||||
- 调用: POST /api/admin/shop-packages
|
||||
- 输入: 套餐 ID + 店铺 ID
|
||||
- 预期: 分配成功
|
||||
|
||||
3. **代理商查看可售套餐**
|
||||
- 角色: 代理商
|
||||
- 调用: GET /api/admin/shop-packages
|
||||
- 预期: 列表包含刚分配的套餐
|
||||
```
|
||||
|
||||
直接转换为测试代码:
|
||||
|
||||
```go
|
||||
func TestFlow_PackageLifecycle(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
var packageID uint
|
||||
|
||||
t.Run("Step1_平台管理员创建套餐", func(t *testing.T) {
|
||||
// POST /api/admin/packages
|
||||
})
|
||||
|
||||
t.Run("Step2_分配给代理商", func(t *testing.T) {
|
||||
// POST /api/admin/shop-packages
|
||||
})
|
||||
|
||||
t.Run("Step3_代理商查看可售套餐", func(t *testing.T) {
|
||||
// GET /api/admin/shop-packages
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```go
|
||||
package flows
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 流程测试:IoT 卡导入到激活完整流程
|
||||
// 来源:openspec/changes/iot-card-management/specs/iot-card/spec.md
|
||||
// 参与者:平台管理员, 系统
|
||||
// ============================================================
|
||||
|
||||
func TestFlow_IotCardImportActivate(t *testing.T) {
|
||||
env := testutils.NewIntegrationTestEnv(t)
|
||||
|
||||
// 流程级共享状态
|
||||
var (
|
||||
taskID string
|
||||
cardICCIDs []string
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 1: 上传 CSV 文件
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/iot-cards/import
|
||||
// 预期: 返回导入任务 ID
|
||||
//
|
||||
// 依赖: 无
|
||||
// 破坏点:如果文件上传不创建异步任务,后续无法追踪进度
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step1_上传CSV文件", func(t *testing.T) {
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
// 创建测试 CSV 内容
|
||||
csvContent := "iccid,msisdn,operator\n" +
|
||||
"89860000000000000001,13800000001,中国移动\n" +
|
||||
"89860000000000000002,13800000002,中国移动\n"
|
||||
|
||||
resp, err := client.UploadFile("POST", "/api/admin/iot-cards/import",
|
||||
"file", "cards.csv", []byte(csvContent))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
err = resp.JSON(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
data := result["data"].(map[string]interface{})
|
||||
taskID = data["task_id"].(string)
|
||||
require.NotEmpty(t, taskID, "任务 ID 不能为空")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 2: 查询导入任务状态
|
||||
// 角色: 平台管理员
|
||||
// 调用: GET /api/admin/iot-cards/import/{taskID}
|
||||
// 预期: 任务状态为 completed,导入成功数量 = 2
|
||||
//
|
||||
// 依赖: Step 1 的 taskID
|
||||
// 破坏点:如果异步任务不更新状态,查询会一直返回 pending
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step2_查询导入状态", func(t *testing.T) {
|
||||
if taskID == "" {
|
||||
t.Skip("依赖 Step 1 创建的 taskID")
|
||||
}
|
||||
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
// 轮询等待任务完成(最多等待 30 秒)
|
||||
var status string
|
||||
for i := 0; i < 30; i++ {
|
||||
resp, err := client.Request("GET",
|
||||
fmt.Sprintf("/api/admin/iot-cards/import/%s", taskID), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
resp.JSON(&result)
|
||||
data := result["data"].(map[string]interface{})
|
||||
status = data["status"].(string)
|
||||
|
||||
if status == "completed" || status == "failed" {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
require.Equal(t, "completed", status, "导入任务应该成功完成")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 3: 验证卡片已入库
|
||||
// 角色: 平台管理员
|
||||
// 调用: GET /api/admin/iot-cards
|
||||
// 预期: 能查询到导入的卡片
|
||||
//
|
||||
// 依赖: Step 2 确认任务完成
|
||||
// 破坏点:如果导入任务不写入数据库,查询不到卡片
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step3_验证卡片入库", func(t *testing.T) {
|
||||
if taskID == "" {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
resp, err := client.Request("GET", "/api/admin/iot-cards", map[string]interface{}{
|
||||
"iccid": "89860000000000000001",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
resp.JSON(&result)
|
||||
data := result["data"].(map[string]interface{})
|
||||
list := data["list"].([]interface{})
|
||||
|
||||
require.Len(t, list, 1, "应该能查询到导入的卡片")
|
||||
|
||||
card := list[0].(map[string]interface{})
|
||||
cardICCIDs = append(cardICCIDs, card["iccid"].(string))
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 4: 激活卡片
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/iot-cards/{iccid}/activate
|
||||
// 预期: 卡片状态变为 active
|
||||
//
|
||||
// 依赖: Step 3 获取的 cardICCIDs
|
||||
// 破坏点:如果激活 API 不调用运营商接口,状态不会真正变化
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step4_激活卡片", func(t *testing.T) {
|
||||
if len(cardICCIDs) == 0 {
|
||||
t.Skip("依赖 Step 3 获取的卡片 ICCID")
|
||||
}
|
||||
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
resp, err := client.Request("POST",
|
||||
fmt.Sprintf("/api/admin/iot-cards/%s/activate", cardICCIDs[0]), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 5: 验证卡片状态
|
||||
// 角色: 平台管理员
|
||||
// 调用: GET /api/admin/iot-cards/{iccid}
|
||||
// 预期: 卡片状态为 active
|
||||
//
|
||||
// 依赖: Step 4 激活操作
|
||||
// 破坏点:如果激活后不更新数据库状态,查询还是旧状态
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step5_验证激活状态", func(t *testing.T) {
|
||||
if len(cardICCIDs) == 0 {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
client := env.AsSuperAdmin()
|
||||
|
||||
resp, err := client.Request("GET",
|
||||
fmt.Sprintf("/api/admin/iot-cards/%s", cardICCIDs[0]), nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
resp.JSON(&result)
|
||||
data := result["data"].(map[string]interface{})
|
||||
|
||||
assert.Equal(t, "active", data["status"], "卡片状态应该是 active")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Step 之间必须按顺序执行吗?
|
||||
|
||||
是的。Go 的 t.Run 保证同一个父测试内的子测试按顺序执行。如果前置 step 失败,后续 step 会因为依赖检查而 skip。
|
||||
|
||||
### Q: 如何处理异步操作?
|
||||
|
||||
使用轮询等待:
|
||||
|
||||
```go
|
||||
// 等待异步任务完成
|
||||
for i := 0; i < maxRetries; i++ {
|
||||
status := checkStatus()
|
||||
if status == "completed" {
|
||||
break
|
||||
}
|
||||
time.Sleep(interval)
|
||||
}
|
||||
```
|
||||
|
||||
### Q: 流程测试太慢怎么办?
|
||||
|
||||
1. 使用 `t.Parallel()` 让不同流程并行(注意数据隔离)
|
||||
2. 减少 sleep 时间,增加轮询频率
|
||||
3. 考虑将部分验证移到验收测试
|
||||
@@ -1,496 +0,0 @@
|
||||
package flows
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 流程测试:一次性佣金链式分配
|
||||
// 来源:openspec/changes/refactor-one-time-commission-allocation/specs/shop-series-allocation/spec.md
|
||||
// 参与者:平台管理员, 一级代理, 二级代理, 三级代理
|
||||
// ============================================================
|
||||
|
||||
func TestFlow_OneTimeCommissionChainAllocation(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// ========================================================
|
||||
// 流程级共享状态
|
||||
// ========================================================
|
||||
var (
|
||||
seriesID uint
|
||||
level1ShopID uint
|
||||
level2ShopID uint
|
||||
level3ShopID uint
|
||||
level1AllocationID uint
|
||||
level2AllocationID uint
|
||||
level3AllocationID uint
|
||||
packageID uint
|
||||
level3PackageAllocID uint
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 1: 平台创建套餐系列并启用一次性佣金
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/package-series
|
||||
// 预期: 返回系列 ID,enable_one_time_commission = true
|
||||
//
|
||||
// 依赖: 无
|
||||
// 破坏点:如果系列创建不支持 enable_one_time_commission,后续分配无法启用
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step1_平台创建套餐系列", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"series_code": fmt.Sprintf("CHAIN_SERIES_%d", time.Now().UnixNano()),
|
||||
"series_name": "链式分配测试系列",
|
||||
"description": "测试一次性佣金链式分配",
|
||||
"status": 1,
|
||||
"enable_one_time_commission": true,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/package-series", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "创建系列失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
seriesID = uint(data["id"].(float64))
|
||||
require.NotZero(t, seriesID, "系列 ID 不能为空")
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 2: 创建三级店铺层级
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/shops (3次)
|
||||
// 预期: 创建一级、二级、三级店铺
|
||||
//
|
||||
// 依赖: 无
|
||||
// 破坏点:如果店铺层级关系不正确,后续分配权限检查会失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step2_创建三级店铺层级", func(t *testing.T) {
|
||||
level1Shop := env.CreateTestShop("一级代理_链式", 1, nil)
|
||||
level1ShopID = level1Shop.ID
|
||||
require.NotZero(t, level1ShopID)
|
||||
|
||||
level2Shop := env.CreateTestShop("二级代理_链式", 2, &level1ShopID)
|
||||
level2ShopID = level2Shop.ID
|
||||
require.NotZero(t, level2ShopID)
|
||||
|
||||
level3Shop := env.CreateTestShop("三级代理_链式", 3, &level2ShopID)
|
||||
level3ShopID = level3Shop.ID
|
||||
require.NotZero(t, level3ShopID)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 3: 平台为一级代理分配系列(金额上限 100 元)
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/shop-series-allocations
|
||||
// 预期: 分配成功,one_time_commission_amount = 10000
|
||||
//
|
||||
// 依赖: Step 1 的 seriesID, Step 2 的 level1ShopID
|
||||
// 破坏点:如果平台无法分配系列,链式分配无法开始
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step3_平台为一级代理分配系列", func(t *testing.T) {
|
||||
if seriesID == 0 || level1ShopID == 0 {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": level1ShopID,
|
||||
"series_id": seriesID,
|
||||
"one_time_commission_amount": 10000,
|
||||
"enable_one_time_commission": true,
|
||||
"one_time_commission_trigger": "accumulated_recharge",
|
||||
"one_time_commission_threshold": 100000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "平台分配失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
level1AllocationID = uint(data["id"].(float64))
|
||||
assert.Equal(t, float64(10000), data["one_time_commission_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 4: 一级代理为二级代理分配系列(金额上限 80 元)
|
||||
// 角色: 一级代理
|
||||
// 调用: POST /api/admin/shop-series-allocations
|
||||
// 预期: 分配成功,one_time_commission_amount = 8000
|
||||
//
|
||||
// 依赖: Step 3 的 level1AllocationID
|
||||
// 破坏点:如果一级无法为下级分配,链式传递中断
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step4_一级为二级分配系列", func(t *testing.T) {
|
||||
if level1AllocationID == 0 {
|
||||
t.Skip("依赖 Step 3")
|
||||
}
|
||||
|
||||
level1Account := env.CreateTestAccount("level1_agent", "password123", constants.UserTypeAgent, &level1ShopID, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": level2ShopID,
|
||||
"series_id": seriesID,
|
||||
"one_time_commission_amount": 8000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(level1Account).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "一级分配给二级失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
level2AllocationID = uint(data["id"].(float64))
|
||||
assert.Equal(t, float64(8000), data["one_time_commission_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 5: 二级代理为三级代理分配系列(金额上限 50 元)
|
||||
// 角色: 二级代理
|
||||
// 调用: POST /api/admin/shop-series-allocations
|
||||
// 预期: 分配成功,one_time_commission_amount = 5000
|
||||
//
|
||||
// 依赖: Step 4 的 level2AllocationID
|
||||
// 破坏点:如果二级无法为下级分配,链式传递中断
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step5_二级为三级分配系列", func(t *testing.T) {
|
||||
if level2AllocationID == 0 {
|
||||
t.Skip("依赖 Step 4")
|
||||
}
|
||||
|
||||
level2Account := env.CreateTestAccount("level2_agent", "password123", constants.UserTypeAgent, &level2ShopID, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": level3ShopID,
|
||||
"series_id": seriesID,
|
||||
"one_time_commission_amount": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(level2Account).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "二级分配给三级失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
level3AllocationID = uint(data["id"].(float64))
|
||||
assert.Equal(t, float64(5000), data["one_time_commission_amount"])
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 6: 验证链式分配金额正确
|
||||
// 角色: 平台管理员
|
||||
// 调用: GET /api/admin/shop-series-allocations?shop_id=xxx&series_id=xxx (3次)
|
||||
// 预期: 一级 10000,二级 8000,三级 5000
|
||||
//
|
||||
// 依赖: Step 3-5 的分配记录
|
||||
// 破坏点:如果金额查询不正确,佣金计算会出错
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step6_验证链式分配金额", func(t *testing.T) {
|
||||
if level3AllocationID == 0 {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
verifyChainAllocationAmount(t, env, level1ShopID, seriesID, 10000)
|
||||
verifyChainAllocationAmount(t, env, level2ShopID, seriesID, 8000)
|
||||
verifyChainAllocationAmount(t, env, level3ShopID, seriesID, 5000)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 7: 平台创建套餐并关联系列
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/packages
|
||||
// 预期: 返回套餐 ID,series_id 正确关联
|
||||
//
|
||||
// 依赖: Step 1 的 seriesID
|
||||
// 破坏点:如果套餐不关联系列,佣金计算无法找到配置
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step7_创建套餐", func(t *testing.T) {
|
||||
if seriesID == 0 {
|
||||
t.Skip("依赖 Step 1")
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"package_code": fmt.Sprintf("CHAIN_PKG_%d", time.Now().UnixNano()),
|
||||
"package_name": "链式分配测试套餐",
|
||||
"series_id": seriesID,
|
||||
"package_type": "formal",
|
||||
"duration_months": 1,
|
||||
"cost_price": 5000,
|
||||
"suggested_retail_price": 9900,
|
||||
"status": 1,
|
||||
"shelf_status": 1,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/packages", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "创建套餐失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
packageID = uint(data["id"].(float64))
|
||||
require.NotZero(t, packageID)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 8: 为三级代理分配套餐(需先有系列分配)
|
||||
// 角色: 平台管理员
|
||||
// 调用: POST /api/admin/shop-package-allocations
|
||||
// 预期: 分配成功,series_allocation_id 关联到系列分配
|
||||
//
|
||||
// 依赖: Step 5 的 level3AllocationID, Step 7 的 packageID
|
||||
// 破坏点:如果套餐分配不检查系列依赖,此测试将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step8_为三级代理分配套餐", func(t *testing.T) {
|
||||
if level3AllocationID == 0 || packageID == 0 {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": level3ShopID,
|
||||
"package_id": packageID,
|
||||
"cost_price": 6000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "套餐分配失败: %s", result.Message)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
level3PackageAllocID = uint(data["id"].(float64))
|
||||
|
||||
if allocID, ok := data["series_allocation_id"]; ok && allocID != nil {
|
||||
assert.Equal(t, float64(level3AllocationID), allocID,
|
||||
"套餐分配应关联到系列分配")
|
||||
}
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Step 9: 验证完整分配链路
|
||||
// 角色: 平台管理员
|
||||
// 调用: GET APIs
|
||||
// 预期: 所有分配记录正确关联
|
||||
//
|
||||
// 依赖: 所有前置步骤
|
||||
// 破坏点:如果任何环节数据不一致,此验证将失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Step9_验证完整分配链路", func(t *testing.T) {
|
||||
if level3PackageAllocID == 0 {
|
||||
t.Skip("依赖前置步骤")
|
||||
}
|
||||
|
||||
assert.NotZero(t, seriesID, "系列已创建")
|
||||
assert.NotZero(t, level1AllocationID, "一级系列分配已创建")
|
||||
assert.NotZero(t, level2AllocationID, "二级系列分配已创建")
|
||||
assert.NotZero(t, level3AllocationID, "三级系列分配已创建")
|
||||
assert.NotZero(t, packageID, "套餐已创建")
|
||||
assert.NotZero(t, level3PackageAllocID, "三级套餐分配已创建")
|
||||
})
|
||||
|
||||
_ = level1AllocationID
|
||||
_ = level3PackageAllocID
|
||||
}
|
||||
|
||||
func TestFlow_OneTimeCommissionChainAllocation_Exceptions(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 异常流程:下级金额超过上级上限
|
||||
// 预期:分配失败,返回错误
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Exception_下级金额超过上级", func(t *testing.T) {
|
||||
parentShop := env.CreateTestShop("超限测试_父级", 1, nil)
|
||||
childShop := env.CreateTestShop("超限测试_子级", 2, &parentShop.ID)
|
||||
series := createFlowTestSeries(t, env, "超限测试系列")
|
||||
|
||||
createFlowPlatformAllocation(t, env, parentShop.ID, series.ID, 10000)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"series_id": series.ID,
|
||||
"one_time_commission_amount": 15000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
parentAccount := env.CreateTestAccount("parent_agent", "password123", constants.UserTypeAgent, &parentShop.ID, nil)
|
||||
|
||||
resp, err := env.AsUser(parentAccount).Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.True(t, resp.StatusCode == 400 || resp.StatusCode == 403,
|
||||
"超限分配应返回 400 或 403,实际: %d", resp.StatusCode)
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 异常流程:未分配系列就分配套餐
|
||||
// 预期:套餐分配失败
|
||||
// ------------------------------------------------------------
|
||||
t.Run("Exception_未分配系列就分配套餐", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("无系列分配店铺", 1, nil)
|
||||
series := createFlowTestSeries(t, env, "未分配系列")
|
||||
pkg := createFlowTestPackage(t, env, series.ID, "未分配测试套餐")
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop.ID,
|
||||
"package_id": pkg.ID,
|
||||
"cost_price": 5000,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-allocations", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 400, resp.StatusCode, "未分配系列时分配套餐应失败")
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 辅助函数
|
||||
// ============================================================
|
||||
|
||||
func verifyChainAllocationAmount(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, expectedAmount int64) {
|
||||
t.Helper()
|
||||
|
||||
path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", shopID, seriesID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", path, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
require.NotEmpty(t, items, "店铺 %d 应存在系列 %d 的分配记录", shopID, seriesID)
|
||||
|
||||
allocation := items[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(expectedAmount), allocation["one_time_commission_amount"],
|
||||
"店铺 %d 的佣金金额应为 %d", shopID, expectedAmount)
|
||||
}
|
||||
|
||||
func createFlowTestSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("FLOW_SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
EnableOneTimeCommission: true,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createFlowTestPackage(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, name string) *model.Package {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
pkg := &model.Package{
|
||||
PackageCode: fmt.Sprintf("FLOW_PKG_%d", timestamp),
|
||||
PackageName: name,
|
||||
SeriesID: seriesID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
CostPrice: 5000,
|
||||
SuggestedRetailPrice: 9900,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(pkg).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func createFlowPlatformAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopSeriesAllocation{
|
||||
ShopID: shopID,
|
||||
SeriesID: seriesID,
|
||||
AllocatorShopID: 0,
|
||||
OneTimeCommissionAmount: amount,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return allocation
|
||||
}
|
||||
@@ -1,406 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
accountSvc "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
accountAuditSvc "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// extractAccountID 从响应 data 中提取账号 ID
|
||||
// gorm.Model 的 ID 字段在 JSON 中序列化为大写 "ID"
|
||||
func extractAccountID(t *testing.T, data map[string]interface{}) uint {
|
||||
t.Helper()
|
||||
idVal := data["ID"]
|
||||
if idVal == nil {
|
||||
idVal = data["id"]
|
||||
}
|
||||
require.NotNil(t, idVal, "响应应包含 ID 字段")
|
||||
return uint(idVal.(float64))
|
||||
}
|
||||
|
||||
// TestAccountAudit 账号操作审计日志集成测试
|
||||
// 验证所有账号管理操作都被正确记录到审计日志
|
||||
func TestAccountAudit(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 13.2 - 创建账号时记录审计日志
|
||||
t.Run("创建账号时记录审计日志", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
// 创建账号
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// 解析响应获取账号 ID
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "创建账号应该成功,响应: %+v", result)
|
||||
require.NotNil(t, result.Data, "响应 data 不应为 nil")
|
||||
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok, "响应 data 应为 map,实际: %T", result.Data)
|
||||
accountID := extractAccountID(t, data)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", accountID, "create").
|
||||
First(&log).Error
|
||||
require.NoError(t, err, "应该存在创建操作的审计日志")
|
||||
|
||||
// 验证日志字段
|
||||
assert.Equal(t, "create", log.OperationType)
|
||||
assert.NotNil(t, log.AfterData, "创建操作应有 after_data")
|
||||
assert.Nil(t, log.BeforeData, "创建操作不应有 before_data")
|
||||
assert.NotNil(t, log.TargetUsername)
|
||||
assert.Equal(t, reqBody.Username, *log.TargetUsername)
|
||||
|
||||
// 验证 after_data 包含账号信息
|
||||
afterData := log.AfterData
|
||||
assert.Equal(t, reqBody.Username, afterData["username"])
|
||||
assert.Equal(t, reqBody.Phone, afterData["phone"])
|
||||
})
|
||||
|
||||
// 13.3 - 更新账号时记录 before_data 和 after_data
|
||||
t.Run("更新账号时记录before_data和after_data", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
account := env.CreateTestAccount("agent_update", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
// 记录原始数据
|
||||
originalUsername := account.Username
|
||||
|
||||
// 更新账号
|
||||
newUsername := fmt.Sprintf("updated_%d", time.Now().UnixNano())
|
||||
reqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", account.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "update").
|
||||
Order("created_at DESC").First(&log).Error
|
||||
require.NoError(t, err, "应该存在更新操作的审计日志")
|
||||
|
||||
// 验证 before_data
|
||||
assert.NotNil(t, log.BeforeData, "更新操作应有 before_data")
|
||||
beforeData := log.BeforeData
|
||||
assert.Equal(t, originalUsername, beforeData["username"])
|
||||
|
||||
// 验证 after_data
|
||||
assert.NotNil(t, log.AfterData, "更新操作应有 after_data")
|
||||
afterData := log.AfterData
|
||||
assert.Equal(t, newUsername, afterData["username"])
|
||||
})
|
||||
|
||||
// 13.4 - 删除账号时记录审计日志
|
||||
t.Run("删除账号时记录审计日志", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
account := env.CreateTestAccount("agent_delete", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
// 删除账号
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/accounts/%d", account.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "delete").
|
||||
First(&log).Error
|
||||
require.NoError(t, err, "应该存在删除操作的审计日志")
|
||||
|
||||
assert.Equal(t, "delete", log.OperationType)
|
||||
assert.NotNil(t, log.BeforeData, "删除操作应有 before_data")
|
||||
assert.Nil(t, log.AfterData, "删除操作不应有 after_data")
|
||||
})
|
||||
|
||||
// 13.5 - 分配角色时记录审计日志
|
||||
t.Run("分配角色时记录审计日志", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
account := env.CreateTestAccount("agent_roles", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
// 代理账号使用 RoleTypeCustomer (2) 类型的角色
|
||||
role := env.CreateTestRole("测试角色", constants.RoleTypeCustomer)
|
||||
|
||||
// 分配角色
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{role.ID},
|
||||
}
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "assign_roles").
|
||||
First(&log).Error
|
||||
require.NoError(t, err, "应该存在分配角色操作的审计日志")
|
||||
|
||||
assert.Equal(t, "assign_roles", log.OperationType)
|
||||
assert.NotNil(t, log.AfterData, "分配角色操作应有 after_data")
|
||||
afterData := log.AfterData
|
||||
roleIDs, ok := afterData["role_ids"].([]interface{})
|
||||
require.True(t, ok, "after_data 应包含 role_ids 数组")
|
||||
assert.Contains(t, roleIDs, float64(role.ID))
|
||||
})
|
||||
|
||||
// 13.6 - 移除角色时记录审计日志
|
||||
// 由于路由参数名与 Handler 不匹配(路由用 :account_id,Handler 用 c.Params("id")),
|
||||
// 此测试创建独立的 AccountService 实例直接调用 RemoveRole 方法来验证审计日志
|
||||
t.Run("移除角色时记录审计日志", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
account := env.CreateTestAccount("agent_remove_role", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
role := env.CreateTestRole("测试角色", constants.RoleTypeCustomer)
|
||||
|
||||
// 先分配角色
|
||||
assignReqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{role.ID},
|
||||
}
|
||||
jsonBody, err := json.Marshal(assignReqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 创建独立的 Service 实例来测试 RemoveRole
|
||||
accountStore := postgres.NewAccountStore(env.TX, env.Redis)
|
||||
roleStore := postgres.NewRoleStore(env.TX)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(env.TX, env.Redis)
|
||||
shopStore := postgres.NewShopStore(env.TX, env.Redis)
|
||||
enterpriseStore := postgres.NewEnterpriseStore(env.TX, env.Redis)
|
||||
shopRoleStore := postgres.NewShopRoleStore(env.TX, env.Redis)
|
||||
auditLogStore := postgres.NewAccountOperationLogStore(env.TX)
|
||||
auditService := accountAuditSvc.NewService(auditLogStore)
|
||||
accountService := accountSvc.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
// 调用 RemoveRole
|
||||
ctx := env.GetSuperAdminContext()
|
||||
err = accountService.RemoveRole(ctx, account.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ? AND operation_type = ?", account.ID, "remove_role").
|
||||
Order("created_at DESC").First(&log).Error
|
||||
require.NoError(t, err, "应该存在移除角色操作的审计日志")
|
||||
|
||||
assert.Equal(t, "remove_role", log.OperationType)
|
||||
assert.NotNil(t, log.AfterData, "移除角色操作应有 after_data")
|
||||
afterData := log.AfterData
|
||||
assert.Equal(t, float64(role.ID), afterData["removed_role_id"])
|
||||
})
|
||||
|
||||
// 13.7 - 审计日志包含完整的操作上下文
|
||||
t.Run("审计日志包含完整的操作上下文", func(t *testing.T) {
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
// 创建账号
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_ctx_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
// 解析响应
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
accountID := extractAccountID(t, data)
|
||||
|
||||
// 等待异步日志写入
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 验证审计日志包含所有上下文
|
||||
var log model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ?", accountID).First(&log).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证操作人信息
|
||||
assert.NotZero(t, log.OperatorID, "应有操作人ID")
|
||||
assert.NotZero(t, log.OperatorType, "应有操作人类型")
|
||||
assert.NotEmpty(t, log.OperatorName, "应有操作人用户名")
|
||||
|
||||
// 验证目标账号信息
|
||||
assert.NotNil(t, log.TargetAccountID, "应有目标账号ID")
|
||||
assert.Equal(t, accountID, *log.TargetAccountID)
|
||||
assert.NotNil(t, log.TargetUsername, "应有目标账号用户名")
|
||||
assert.NotNil(t, log.TargetUserType, "应有目标账号类型")
|
||||
|
||||
// 验证请求上下文(集成测试中 RequestID/IP/UserAgent 可能为空,因为使用 httptest)
|
||||
// 但在真实环境中这些字段会被填充
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// TestAccountAudit_AsyncNotBlock 13.8 - 审计日志写入失败不影响业务操作
|
||||
// 使用独立环境避免与其他测试的异步 goroutine 冲突
|
||||
func TestAccountAudit_AsyncNotBlock(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_async_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code, "业务操作应该成功,不受审计日志影响")
|
||||
assert.NotNil(t, result.Data, "应返回创建的账号数据")
|
||||
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
accountID := extractAccountID(t, data)
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
var account model.Account
|
||||
err = env.RawDB().First(&account, accountID).Error
|
||||
require.NoError(t, err, "账号应该被成功创建到数据库")
|
||||
assert.Equal(t, reqBody.Username, account.Username)
|
||||
}
|
||||
|
||||
// TestAccountAudit_OperationTypes 13.9 - 验证操作类型正确性
|
||||
// 使用独立环境避免与其他测试的异步 goroutine 冲突
|
||||
func TestAccountAudit_OperationTypes(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
createReqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_optype_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
jsonBody, err := json.Marshal(createReqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
accountID := extractAccountID(t, data)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
newUsername := fmt.Sprintf("updated_optype_%d", time.Now().UnixNano())
|
||||
updateReqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
jsonBody, err = json.Marshal(updateReqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err = env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", accountID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
var logs []model.AccountOperationLog
|
||||
err = env.RawDB().Where("target_account_id = ?", accountID).
|
||||
Order("created_at ASC").Find(&logs).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
require.GreaterOrEqual(t, len(logs), 2, "应该至少有 create 和 update 两条审计日志")
|
||||
|
||||
operationTypes := make(map[string]bool)
|
||||
for _, log := range logs {
|
||||
operationTypes[log.OperationType] = true
|
||||
}
|
||||
|
||||
assert.True(t, operationTypes["create"], "应该有 create 类型的审计日志")
|
||||
assert.True(t, operationTypes["update"], "应该有 update 类型的审计日志")
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
// TestAccountPermission_12_2 企业账号访问账号管理接口被路由层拦截
|
||||
func TestAccountPermission_12_2(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise("测试企业", &shop.ID)
|
||||
|
||||
enterpriseAccount := env.CreateTestAccount(
|
||||
"enterprise_user",
|
||||
"password123",
|
||||
constants.UserTypeEnterprise,
|
||||
nil,
|
||||
&enterprise.ID,
|
||||
)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
EnterpriseID: &enterprise.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(enterpriseAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode, "企业账号应被路由层拦截")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "权限", "错误消息应包含权限相关信息")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_3 代理账号创建自己店铺的账号成功
|
||||
func TestAccountPermission_12_3(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("代理店铺1", 1, nil)
|
||||
|
||||
agentAccount := env.CreateTestAccount(
|
||||
"agent_user",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("agent_same_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "代理账号应能创建自己店铺的账号")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code, "业务码应为0")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_4 代理账号创建下级店铺的账号成功
|
||||
func TestAccountPermission_12_4(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("父店铺", 1, nil)
|
||||
childShop := env.CreateTestShop("子店铺", 2, &parentShop.ID)
|
||||
|
||||
agentAccount := env.CreateTestAccount(
|
||||
"agent_parent",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&parentShop.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("agent_child_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &childShop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "代理账号应能创建下级店铺的账号")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code, "业务码应为0")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_5 代理账号创建其他店铺的账号失败
|
||||
func TestAccountPermission_12_5(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop1 := env.CreateTestShop("独立店铺1", 1, nil)
|
||||
shop2 := env.CreateTestShop("独立店铺2", 1, nil)
|
||||
|
||||
agentAccount := env.CreateTestAccount(
|
||||
"agent_shop1",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop1.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("agent_other_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode, "代理账号不应能创建其他店铺的账号")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "权限", "错误消息应包含权限相关信息")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_6 代理账号创建平台账号失败
|
||||
func TestAccountPermission_12_6(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("代理店铺", 1, nil)
|
||||
|
||||
agentAccount := env.CreateTestAccount(
|
||||
"agent_try_platform",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("platform_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypePlatform,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode, "代理账号不应能创建平台账号")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "权限", "错误消息应包含权限相关信息")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_7 平台账号创建任意类型账号成功
|
||||
func TestAccountPermission_12_7(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("平台测试店铺", 1, nil)
|
||||
|
||||
platformAccount := env.CreateTestAccount(
|
||||
"platform_user",
|
||||
"password123",
|
||||
constants.UserTypePlatform,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
t.Run("创建平台账号", func(t *testing.T) {
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("platform_new_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypePlatform,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(platformAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "平台账号应能创建平台账号")
|
||||
})
|
||||
|
||||
t.Run("创建代理账号", func(t *testing.T) {
|
||||
time.Sleep(time.Millisecond)
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("agent_new_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(platformAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "平台账号应能创建代理账号")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_8 超级管理员创建任意类型账号成功
|
||||
func TestAccountPermission_12_8(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("超管测试店铺", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise("超管测试企业", &shop.ID)
|
||||
|
||||
t.Run("创建平台账号", func(t *testing.T) {
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("superadmin_platform_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypePlatform,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "超级管理员应能创建平台账号")
|
||||
})
|
||||
|
||||
t.Run("创建代理账号", func(t *testing.T) {
|
||||
time.Sleep(time.Millisecond)
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("superadmin_agent_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "超级管理员应能创建代理账号")
|
||||
})
|
||||
|
||||
t.Run("创建企业账号", func(t *testing.T) {
|
||||
time.Sleep(time.Millisecond)
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("superadmin_ent_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
EnterpriseID: &enterprise.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "超级管理员应能创建企业账号")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_9 查询不存在的账号返回统一错误
|
||||
func TestAccountPermission_12_9(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("查询测试店铺", 1, nil)
|
||||
agentAccount := env.CreateTestAccount(
|
||||
"agent_query",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/accounts/99999", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// GORM 自动过滤后,查询不存在的账号返回 "账号不存在" (400)
|
||||
// 或 "无权限操作该资源或资源不存在" (403)
|
||||
assert.True(t,
|
||||
resp.StatusCode == fiber.StatusBadRequest ||
|
||||
resp.StatusCode == fiber.StatusForbidden ||
|
||||
resp.StatusCode == fiber.StatusNotFound,
|
||||
"查询不存在的账号应返回错误状态码")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "业务码应不为0")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_10 查询越权的账号返回相同错误消息
|
||||
func TestAccountPermission_12_10(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop1 := env.CreateTestShop("越权测试店铺1", 1, nil)
|
||||
shop2 := env.CreateTestShop("越权测试店铺2", 1, nil)
|
||||
|
||||
agentAccount1 := env.CreateTestAccount(
|
||||
"agent_auth1",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop1.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
agentAccount2 := env.CreateTestAccount(
|
||||
"agent_auth2",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop2.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
resp, err := env.AsUser(agentAccount1).Request(
|
||||
"GET",
|
||||
fmt.Sprintf("/api/admin/accounts/%d", agentAccount2.ID),
|
||||
nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// GORM 自动过滤使越权查询返回与不存在相同的错误,防止信息泄露
|
||||
assert.True(t,
|
||||
resp.StatusCode == fiber.StatusBadRequest ||
|
||||
resp.StatusCode == fiber.StatusForbidden ||
|
||||
resp.StatusCode == fiber.StatusNotFound,
|
||||
"查询越权账号应返回错误状态码")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code, "业务码应不为0")
|
||||
}
|
||||
|
||||
// TestAccountPermission_12_11 代理账号更新其他店铺的账号失败
|
||||
func TestAccountPermission_12_11(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop1 := env.CreateTestShop("更新测试店铺1", 1, nil)
|
||||
shop2 := env.CreateTestShop("更新测试店铺2", 1, nil)
|
||||
|
||||
agentAccount1 := env.CreateTestAccount(
|
||||
"agent_update1",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop1.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
agentAccount2 := env.CreateTestAccount(
|
||||
"agent_update2",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&shop2.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
newUsername := "updated_username"
|
||||
reqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount1).Request(
|
||||
"PUT",
|
||||
fmt.Sprintf("/api/admin/accounts/%d", agentAccount2.ID),
|
||||
jsonBody,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode, "代理账号不应能更新其他店铺的账号")
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Message, "权限", "错误消息应包含权限相关信息")
|
||||
}
|
||||
|
||||
// TestEnterpriseAccountRouteBlocking 测试企业账号访问各类型账号管理接口的路由层拦截
|
||||
func TestEnterpriseAccountRouteBlocking(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("路由拦截测试店铺", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise("路由拦截测试企业", &shop.ID)
|
||||
|
||||
enterpriseAccount := env.CreateTestAccount(
|
||||
"enterprise_route_test",
|
||||
"password123",
|
||||
constants.UserTypeEnterprise,
|
||||
nil,
|
||||
&enterprise.ID,
|
||||
)
|
||||
|
||||
t.Run("企业账号访问企业账号列表接口被拦截", func(t *testing.T) {
|
||||
resp, err := env.AsUser(enterpriseAccount).Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("企业账号访问企业账号详情接口被拦截", func(t *testing.T) {
|
||||
resp, err := env.AsUser(enterpriseAccount).Request("GET", "/api/admin/accounts/1", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("企业账号访问创建企业账号接口被拦截", func(t *testing.T) {
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("test_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
EnterpriseID: &enterprise.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(enterpriseAccount).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAgentAccountHierarchyPermission 测试代理账号的层级权限
|
||||
func TestAgentAccountHierarchyPermission(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
level1Shop := env.CreateTestShop("一级店铺", 1, nil)
|
||||
level2Shop := env.CreateTestShop("二级店铺", 2, &level1Shop.ID)
|
||||
level3Shop := env.CreateTestShop("三级店铺", 3, &level2Shop.ID)
|
||||
|
||||
level2Agent := env.CreateTestAccount(
|
||||
"level2_agent",
|
||||
"password123",
|
||||
constants.UserTypeAgent,
|
||||
&level2Shop.ID,
|
||||
nil,
|
||||
)
|
||||
|
||||
t.Run("二级代理可以管理三级店铺账号", func(t *testing.T) {
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("level3_new_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &level3Shop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(level2Agent).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode, "二级代理应能管理三级店铺账号")
|
||||
})
|
||||
|
||||
t.Run("二级代理不能管理一级店铺账号", func(t *testing.T) {
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: fmt.Sprintf("level1_new_%d", time.Now().UnixNano()),
|
||||
Phone: fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000),
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &level1Shop.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(level2Agent).Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusForbidden, resp.StatusCode, "二级代理不应能管理一级店铺账号")
|
||||
})
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
accountService "github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
accountAuditService "github.com/break/junhong_cmp_fiber/internal/service/account_audit"
|
||||
postgresStore "github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
// TestAccountRoleAssociation_AssignRoles 测试账号角色分配功能
|
||||
func TestAccountRoleAssociation_AssignRoles(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 初始化 Store 和 Service
|
||||
accountStore := postgresStore.NewAccountStore(env.TX, env.Redis)
|
||||
roleStore := postgresStore.NewRoleStore(env.TX)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(env.TX, env.Redis)
|
||||
shopStore := postgresStore.NewShopStore(env.TX, env.Redis)
|
||||
enterpriseStore := postgresStore.NewEnterpriseStore(env.TX, env.Redis)
|
||||
shopRoleStore := postgresStore.NewShopRoleStore(env.TX, env.Redis)
|
||||
auditLogStore := postgresStore.NewAccountOperationLogStore(env.TX)
|
||||
auditService := accountAuditService.NewService(auditLogStore)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
// 获取超级管理员上下文
|
||||
userCtx := env.GetSuperAdminContext()
|
||||
|
||||
t.Run("成功分配单个角色", func(t *testing.T) {
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "single_role_test",
|
||||
Phone: "13800000100",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "单角色测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 分配角色
|
||||
ars, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 1)
|
||||
assert.Equal(t, account.ID, ars[0].AccountID)
|
||||
assert.Equal(t, role.ID, ars[0].RoleID)
|
||||
})
|
||||
|
||||
t.Run("成功分配多个角色", func(t *testing.T) {
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "multi_role_test",
|
||||
Phone: "13800000101",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
// 创建多个测试角色
|
||||
roles := make([]*model.Role, 3)
|
||||
roleIDs := make([]uint, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
roles[i] = &model.Role{
|
||||
RoleName: "多角色测试_" + string(rune('A'+i)),
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(roles[i])
|
||||
roleIDs[i] = roles[i].ID
|
||||
}
|
||||
|
||||
// 分配角色
|
||||
ars, err := accService.AssignRoles(userCtx, account.ID, roleIDs)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 3)
|
||||
})
|
||||
|
||||
t.Run("获取账号的角色列表", func(t *testing.T) {
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "get_roles_test",
|
||||
Phone: "13800000102",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
// 创建并分配角色
|
||||
role := &model.Role{
|
||||
RoleName: "获取角色列表测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
_, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 获取角色列表
|
||||
roles, err := accService.GetRoles(userCtx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, roles, 1)
|
||||
assert.Equal(t, role.ID, roles[0].ID)
|
||||
})
|
||||
|
||||
t.Run("移除账号的角色", func(t *testing.T) {
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "remove_role_test",
|
||||
Phone: "13800000103",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
// 创建并分配角色
|
||||
role := &model.Role{
|
||||
RoleName: "移除角色测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
_, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 移除角色
|
||||
err = accService.RemoveRole(userCtx, account.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证角色已被软删除
|
||||
var ar model.AccountRole
|
||||
err = env.RawDB().Unscoped().Where("account_id = ? AND role_id = ?", account.ID, role.ID).First(&ar).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, ar.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("重复分配角色不会创建重复记录", func(t *testing.T) {
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "duplicate_role_test",
|
||||
Phone: "13800000104",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "重复分配测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 第一次分配
|
||||
_, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 第二次分配相同角色
|
||||
_, err = accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证只有一条记录
|
||||
var count int64
|
||||
env.RawDB().Model(&model.AccountRole{}).Where("account_id = ? AND role_id = ?", account.ID, role.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
|
||||
t.Run("账号不存在时分配角色失败", func(t *testing.T) {
|
||||
role := &model.Role{
|
||||
RoleName: "账号不存在测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
_, err := accService.AssignRoles(userCtx, 99999, []uint{role.ID})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("角色不存在时分配失败", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
Username: "role_not_exist_test",
|
||||
Phone: "13800000105",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
_, err := accService.AssignRoles(userCtx, account.ID, []uint{99999})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountRoleAssociation_SoftDelete 测试软删除对账号角色关联的影响
|
||||
func TestAccountRoleAssociation_SoftDelete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 初始化 Store 和 Service
|
||||
accountStore := postgresStore.NewAccountStore(env.TX, env.Redis)
|
||||
roleStore := postgresStore.NewRoleStore(env.TX)
|
||||
accountRoleStore := postgresStore.NewAccountRoleStore(env.TX, env.Redis)
|
||||
shopStore := postgresStore.NewShopStore(env.TX, env.Redis)
|
||||
shopRoleStore := postgresStore.NewShopRoleStore(env.TX, env.Redis)
|
||||
enterpriseStore := postgresStore.NewEnterpriseStore(env.TX, env.Redis)
|
||||
auditLogStore := postgresStore.NewAccountOperationLogStore(env.TX)
|
||||
auditService := accountAuditService.NewService(auditLogStore)
|
||||
accService := accountService.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
// 获取超级管理员上下文
|
||||
userCtx := env.GetSuperAdminContext()
|
||||
|
||||
t.Run("软删除角色后重新分配可以恢复", func(t *testing.T) {
|
||||
// 创建测试数据
|
||||
account := &model.Account{
|
||||
Username: "restore_role_test",
|
||||
Phone: "13800000200",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
role := &model.Role{
|
||||
RoleName: "恢复角色测试",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 分配角色
|
||||
_, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 移除角色
|
||||
err = accService.RemoveRole(userCtx, account.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 重新分配角色
|
||||
ars, err := accService.AssignRoles(userCtx, account.ID, []uint{role.ID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 1)
|
||||
|
||||
// 验证关联已恢复
|
||||
roles, err := accService.GetRoles(userCtx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, roles, 1)
|
||||
})
|
||||
}
|
||||
@@ -1,936 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 平台账号管理测试
|
||||
// =============================================================================
|
||||
|
||||
func TestPlatformAccount_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
username := fmt.Sprintf("platform_user_%d", time.Now().UnixNano())
|
||||
phone := fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypePlatform,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var count int64
|
||||
env.RawDB().Model(&model.Account{}).Where("username = ?", username).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_Create_DuplicateUsername(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
existingAccount := env.CreateTestAccount("existing_platform", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
phone := fmt.Sprintf("138%08d", time.Now().UnixNano()%100000000)
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: existingAccount.Username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypePlatform,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
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, errors.CodeUsernameExists, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
env.CreateTestAccount(fmt.Sprintf("platform_list_%d", i), "password123", constants.UserTypePlatform, nil, nil)
|
||||
}
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 3)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_Get(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testAccount := env.CreateTestAccount("platform_detail", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_Get_NotFound(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts/99999", nil)
|
||||
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, errors.CodeAccountNotFound, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testAccount := env.CreateTestAccount("platform_update", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
newUsername := fmt.Sprintf("updated_%d", time.Now().UnixNano())
|
||||
reqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_UpdatePassword(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testAccount := env.CreateTestAccount("platform_pwd", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
reqBody := dto.UpdatePasswordRequest{
|
||||
NewPassword: "NewPassword@123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/password", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_UpdateStatus(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testAccount := env.CreateTestAccount("platform_status", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
reqBody := dto.UpdateStatusRequest{
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/status", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestPlatformAccount_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testAccount := env.CreateTestAccount("platform_delete", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 代理账号管理测试
|
||||
// =============================================================================
|
||||
|
||||
func TestShopAccount_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理账号测试店铺", 1, nil)
|
||||
|
||||
username := fmt.Sprintf("agent_user_%d", time.Now().UnixNano())
|
||||
phone := fmt.Sprintf("139%08d", time.Now().UnixNano()%100000000)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &testShop.ID,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
assert.NotNil(t, result.Data)
|
||||
}
|
||||
|
||||
func TestShopAccount_Create_MissingShopID(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
username := fmt.Sprintf("agent_no_shop_%d", time.Now().UnixNano())
|
||||
phone := fmt.Sprintf("139%08d", time.Now().UnixNano()%100000000)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
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, "创建代理账号缺少店铺ID应返回错误")
|
||||
}
|
||||
|
||||
func TestShopAccount_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理列表测试店铺", 1, nil)
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
env.CreateTestAccount(fmt.Sprintf("agent_list_%d", i), "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
}
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 3)
|
||||
}
|
||||
|
||||
func TestShopAccount_Get(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理详情测试店铺", 1, nil)
|
||||
testAccount := env.CreateTestAccount("agent_detail", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestShopAccount_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理更新测试店铺", 1, nil)
|
||||
testAccount := env.CreateTestAccount("agent_update", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
newUsername := fmt.Sprintf("updated_agent_%d", time.Now().UnixNano())
|
||||
reqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestShopAccount_UpdatePassword(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理密码测试店铺", 1, nil)
|
||||
testAccount := env.CreateTestAccount("agent_pwd", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
reqBody := dto.UpdatePasswordRequest{
|
||||
NewPassword: "NewPassword@456",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/password", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestShopAccount_UpdateStatus(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理状态测试店铺", 1, nil)
|
||||
testAccount := env.CreateTestAccount("agent_status", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
reqBody := dto.UpdateStatusRequest{
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/status", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestShopAccount_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理删除测试店铺", 1, nil)
|
||||
testAccount := env.CreateTestAccount("agent_delete", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 企业账号管理测试
|
||||
// =============================================================================
|
||||
|
||||
func TestEnterpriseAccount_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业账号测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("测试企业", &testShop.ID)
|
||||
|
||||
username := fmt.Sprintf("enterprise_user_%d", time.Now().UnixNano())
|
||||
phone := fmt.Sprintf("137%08d", time.Now().UnixNano()%100000000)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
EnterpriseID: &testEnterprise.ID,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
assert.NotNil(t, result.Data)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_Create_MissingEnterpriseID(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
username := fmt.Sprintf("enterprise_no_ent_%d", time.Now().UnixNano())
|
||||
phone := fmt.Sprintf("137%08d", time.Now().UnixNano()%100000000)
|
||||
|
||||
reqBody := dto.CreateAccountRequest{
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: "Password123",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", jsonBody)
|
||||
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, "创建企业账号缺少企业ID应返回错误")
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业列表测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("列表测试企业", &testShop.ID)
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
env.CreateTestAccount(fmt.Sprintf("ent_list_%d", i), "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
}
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 3)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_Get(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业详情测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("详情测试企业", &testShop.ID)
|
||||
testAccount := env.CreateTestAccount("ent_detail", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业更新测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("更新测试企业", &testShop.ID)
|
||||
testAccount := env.CreateTestAccount("ent_update", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
newUsername := fmt.Sprintf("updated_ent_%d", time.Now().UnixNano())
|
||||
reqBody := dto.UpdateAccountRequest{
|
||||
Username: &newUsername,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_UpdatePassword(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业密码测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("密码测试企业", &testShop.ID)
|
||||
testAccount := env.CreateTestAccount("ent_pwd", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
reqBody := dto.UpdatePasswordRequest{
|
||||
NewPassword: "NewPassword@789",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/password", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_UpdateStatus(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业状态测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("状态测试企业", &testShop.ID)
|
||||
testAccount := env.CreateTestAccount("ent_status", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
reqBody := dto.UpdateStatusRequest{
|
||||
Status: constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/accounts/%d/status", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业删除测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("删除测试企业", &testShop.ID)
|
||||
testAccount := env.CreateTestAccount("ent_delete", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/accounts/%d", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestEnterpriseAccount_Forbidden(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业禁止测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("禁止测试企业", &testShop.ID)
|
||||
entAccount := env.CreateTestAccount("ent_forbidden", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
resp, err := env.AsUser(entAccount).Request("GET", "/api/admin/accounts", nil)
|
||||
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, errors.CodeForbidden, result.Code, "企业用户应禁止访问账号管理功能")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 角色管理测试(所有账号类型)
|
||||
// =============================================================================
|
||||
|
||||
func TestAccount_AssignRoles_Platform(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformRole := env.CreateTestRole("平台角色", constants.RoleTypePlatform)
|
||||
testAccount := env.CreateTestAccount("role_platform", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{platformRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_GetRoles_Platform(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformRole := env.CreateTestRole("平台角色获取", constants.RoleTypePlatform)
|
||||
testAccount := env.CreateTestAccount("role_platform_get", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: testAccount.ID,
|
||||
RoleID: platformRole.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
env.TX.Create(accountRole)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_ClearRoles_Platform(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformRole := env.CreateTestRole("平台角色清空", constants.RoleTypePlatform)
|
||||
testAccount := env.CreateTestAccount("role_platform_clr", "password123", constants.UserTypePlatform, nil, nil)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: testAccount.ID,
|
||||
RoleID: platformRole.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
env.TX.Create(accountRole)
|
||||
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_SuperAdminCannotAssignRoles(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformRole := env.CreateTestRole("禁止分配角色", constants.RoleTypePlatform)
|
||||
superAdmin := env.CreateTestAccount("superadmin_role", "password123", constants.UserTypeSuperAdmin, nil, nil)
|
||||
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{platformRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", superAdmin.ID), jsonBody)
|
||||
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, errors.CodeInvalidParam, result.Code)
|
||||
assert.Contains(t, result.Message, "超级管理员不允许分配角色")
|
||||
}
|
||||
|
||||
func TestAccount_AssignRoles_Shop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理角色测试店铺", 1, nil)
|
||||
agentRole := env.CreateTestRole("代理角色", constants.RoleTypeCustomer)
|
||||
testAccount := env.CreateTestAccount("role_agent", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{agentRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_GetRoles_Shop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("代理获取角色测试店铺", 1, nil)
|
||||
agentRole := env.CreateTestRole("代理获取角色", constants.RoleTypeCustomer)
|
||||
testAccount := env.CreateTestAccount("role_agent_get", "password123", constants.UserTypeAgent, &testShop.ID, nil)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: testAccount.ID,
|
||||
RoleID: agentRole.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
env.TX.Create(accountRole)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_AssignRoles_Enterprise(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业角色测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("角色测试企业", &testShop.ID)
|
||||
entRole := env.CreateTestRole("企业角色", constants.RoleTypeCustomer)
|
||||
testAccount := env.CreateTestAccount("role_ent", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
reqBody := dto.AssignRolesRequest{
|
||||
RoleIDs: []uint{entRole.ID},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
func TestAccount_GetRoles_Enterprise(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testShop := env.CreateTestShop("企业获取角色测试店铺", 1, nil)
|
||||
testEnterprise := env.CreateTestEnterprise("获取角色测试企业", &testShop.ID)
|
||||
entRole := env.CreateTestRole("企业获取角色", constants.RoleTypeCustomer)
|
||||
testAccount := env.CreateTestAccount("role_ent_get", "password123", constants.UserTypeEnterprise, nil, &testEnterprise.ID)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: testAccount.ID,
|
||||
RoleID: entRole.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
env.TX.Create(accountRole)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", testAccount.ID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 通用场景测试
|
||||
// =============================================================================
|
||||
|
||||
func TestAccount_Unauthorized_Platform(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestAccount_Unauthorized_Shop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestAccount_Unauthorized_Enterprise(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestAccount_InvalidID(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts/invalid", nil)
|
||||
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, errors.CodeInvalidParam, result.Code)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 关联查询测试
|
||||
// =============================================================================
|
||||
|
||||
func TestAccountList_FilterByShopID_WithShopName(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop1 := env.CreateTestShop("测试店铺A", 1, nil)
|
||||
shop2 := env.CreateTestShop("测试店铺B", 1, nil)
|
||||
|
||||
account1 := env.CreateTestAccount("shop_account_1", "password123", constants.UserTypeAgent, &shop1.ID, nil)
|
||||
account2 := env.CreateTestAccount("shop_account_2", "password123", constants.UserTypeAgent, &shop1.ID, nil)
|
||||
account3 := env.CreateTestAccount("shop_account_3", "password123", constants.UserTypeAgent, &shop2.ID, nil)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/accounts?shop_id=%d&page=1&page_size=10", shop1.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 2)
|
||||
|
||||
foundAccount1 := false
|
||||
foundAccount2 := false
|
||||
for _, item := range items {
|
||||
accountData := item.(map[string]interface{})
|
||||
accountID := uint(accountData["id"].(float64))
|
||||
|
||||
if accountID == account1.ID || accountID == account2.ID {
|
||||
assert.Equal(t, float64(shop1.ID), accountData["shop_id"])
|
||||
assert.Equal(t, shop1.ShopName, accountData["shop_name"])
|
||||
|
||||
if accountID == account1.ID {
|
||||
foundAccount1 = true
|
||||
}
|
||||
if accountID == account2.ID {
|
||||
foundAccount2 = true
|
||||
}
|
||||
}
|
||||
|
||||
if accountID == account3.ID {
|
||||
t.Errorf("不应该返回 shop2 的账号,但返回了账号 %d", account3.ID)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundAccount1, "应该返回 account1")
|
||||
assert.True(t, foundAccount2, "应该返回 account2")
|
||||
}
|
||||
|
||||
func TestAccountList_FilterByEnterpriseID_WithEnterpriseName(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("归属店铺", 1, nil)
|
||||
enterprise1 := env.CreateTestEnterprise("测试企业A", &shop.ID)
|
||||
enterprise2 := env.CreateTestEnterprise("测试企业B", &shop.ID)
|
||||
|
||||
account1 := env.CreateTestAccount("enterprise_account_1", "password123", constants.UserTypeEnterprise, nil, &enterprise1.ID)
|
||||
account2 := env.CreateTestAccount("enterprise_account_2", "password123", constants.UserTypeEnterprise, nil, &enterprise2.ID)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/accounts?enterprise_id=%d&page=1&page_size=10", enterprise1.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.GreaterOrEqual(t, len(items), 1)
|
||||
|
||||
foundAccount1 := false
|
||||
for _, item := range items {
|
||||
accountData := item.(map[string]interface{})
|
||||
accountID := uint(accountData["id"].(float64))
|
||||
|
||||
if accountID == account1.ID {
|
||||
foundAccount1 = true
|
||||
assert.Equal(t, float64(enterprise1.ID), accountData["enterprise_id"])
|
||||
assert.Equal(t, enterprise1.EnterpriseName, accountData["enterprise_name"])
|
||||
}
|
||||
|
||||
if accountID == account2.ID {
|
||||
t.Errorf("不应该返回 enterprise2 的账号,但返回了账号 %d", account2.ID)
|
||||
}
|
||||
}
|
||||
|
||||
assert.True(t, foundAccount1, "应该返回 account1")
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
// TestAPIRegression_AllEndpointsAccessible 测试所有 API 端点在重构后仍可访问
|
||||
func TestAPIRegression_AllEndpointsAccessible(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 定义所有需要测试的端点(检测端点是否存在,不检测业务逻辑)
|
||||
endpoints := []struct {
|
||||
method string
|
||||
path string
|
||||
name string
|
||||
requiresAuth bool
|
||||
}{
|
||||
// Health endpoints(无需认证)
|
||||
{"GET", "/health", "Health check", false},
|
||||
|
||||
// Account endpoints(需要认证)
|
||||
{"GET", "/api/admin/accounts", "List accounts", true},
|
||||
{"GET", "/api/admin/accounts/1", "Get account", true},
|
||||
|
||||
// Role endpoints(需要认证)
|
||||
{"GET", "/api/admin/roles", "List roles", true},
|
||||
{"GET", "/api/admin/roles/1", "Get role", true},
|
||||
|
||||
// Permission endpoints(需要认证)
|
||||
{"GET", "/api/admin/permissions", "List permissions", true},
|
||||
{"GET", "/api/admin/permissions/1", "Get permission", true},
|
||||
{"GET", "/api/admin/permissions/tree", "Get permission tree", true},
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
t.Run(ep.name, func(t *testing.T) {
|
||||
var resp *httptest.ResponseRecorder
|
||||
var err error
|
||||
|
||||
if ep.requiresAuth {
|
||||
httpResp, httpErr := env.AsSuperAdmin().Request(ep.method, ep.path, nil)
|
||||
require.NoError(t, httpErr)
|
||||
resp = &httptest.ResponseRecorder{Code: httpResp.StatusCode}
|
||||
err = nil
|
||||
} else {
|
||||
req := httptest.NewRequest(ep.method, ep.path, nil)
|
||||
httpResp, httpErr := env.App.Test(req)
|
||||
require.NoError(t, httpErr)
|
||||
resp = &httptest.ResponseRecorder{Code: httpResp.StatusCode}
|
||||
err = httpErr
|
||||
}
|
||||
_ = err
|
||||
|
||||
// 验证端点可访问(状态码不是 404 或 500)
|
||||
assert.NotEqual(t, fiber.StatusNotFound, resp.Code,
|
||||
"端点 %s %s 应该存在", ep.method, ep.path)
|
||||
assert.NotEqual(t, fiber.StatusInternalServerError, resp.Code,
|
||||
"端点 %s %s 不应该返回 500 错误", ep.method, ep.path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRegression_RouteModularization(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("账号模块路由正常", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
Username: "regression_test",
|
||||
Phone: "13800000300",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d", account.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/accounts/%d/roles", account.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("角色模块路由正常", func(t *testing.T) {
|
||||
role := &model.Role{
|
||||
RoleName: "回归测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/roles/%d", role.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/roles/%d/permissions", role.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("权限模块路由正常", func(t *testing.T) {
|
||||
perm := &model.Permission{
|
||||
PermName: "回归测试权限",
|
||||
PermCode: "regression:perm",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/permissions/%d", perm.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = env.AsSuperAdmin().Request("GET", "/api/admin/permissions/tree", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIRegression_ErrorHandling 测试错误处理在重构后仍正常
|
||||
func TestAPIRegression_ErrorHandling(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("资源不存在返回正确错误码", func(t *testing.T) {
|
||||
// 账号不存在
|
||||
req := httptest.NewRequest("GET", "/api/admin/accounts/99999", nil)
|
||||
resp, err := env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
// 应该返回业务错误,不是 404
|
||||
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// 角色不存在
|
||||
req = httptest.NewRequest("GET", "/api/admin/roles/99999", nil)
|
||||
resp, err = env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
|
||||
|
||||
// 权限不存在
|
||||
req = httptest.NewRequest("GET", "/api/admin/permissions/99999", nil)
|
||||
resp, err = env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("无效参数返回正确错误码", func(t *testing.T) {
|
||||
// 无效账号 ID
|
||||
req := httptest.NewRequest("GET", "/api/admin/accounts/invalid", nil)
|
||||
resp, err := env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, fiber.StatusInternalServerError, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRegression_Pagination(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
for i := 1; i <= 25; i++ {
|
||||
account := &model.Account{
|
||||
Username: fmt.Sprintf("pagination_test_%d", i),
|
||||
Phone: fmt.Sprintf("138000004%02d", i),
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(account)
|
||||
}
|
||||
|
||||
t.Run("分页参数正常工作", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
resp, err = env.AsSuperAdmin().Request("GET", "/api/admin/accounts?page=2&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("默认分页参数工作", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRegression_ResponseFormat(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("成功响应包含正确字段", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/accounts", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Type"), "application/json")
|
||||
})
|
||||
|
||||
t.Run("健康检查端点响应正常", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/health", nil)
|
||||
resp, err := env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIRegression_ServicesIntegration 测试服务集成在重构后仍正常
|
||||
func TestAPIRegression_ServicesIntegration(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("Services 容器正确初始化", func(t *testing.T) {
|
||||
// 验证所有模块路由都已注册
|
||||
endpoints := []string{
|
||||
"/health",
|
||||
"/api/admin/accounts",
|
||||
"/api/admin/roles",
|
||||
"/api/admin/permissions",
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
req := httptest.NewRequest("GET", ep, nil)
|
||||
resp, err := env.App.Test(req)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, fiber.StatusNotFound, resp.StatusCode,
|
||||
"端点 %s 应该已注册", ep)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,478 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthorization_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
ts := time.Now().Unix() % 100000
|
||||
shop := env.CreateTestShop("AUTH_TEST_SHOP", 1, nil)
|
||||
|
||||
enterprise := env.CreateTestEnterprise("AUTH_TEST_ENTERPRISE", &shop.ID)
|
||||
|
||||
card1 := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("AC1%d", ts),
|
||||
MSISDN: "13800001001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
card2 := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("AC2%d", ts),
|
||||
MSISDN: "13800001002",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card1).Error)
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
now := time.Now()
|
||||
auth1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card1.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "集成测试授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(auth1).Error)
|
||||
|
||||
t.Run("平台用户获取授权记录列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/authorizations?page=1&page_size=20", nil)
|
||||
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)
|
||||
|
||||
data, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
items, ok := data["items"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.GreaterOrEqual(t, len(items), 1)
|
||||
})
|
||||
|
||||
t.Run("按企业ID筛选授权记录", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations?enterprise_id=%d&page=1&page_size=20", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
total := int(data["total"].(float64))
|
||||
assert.Equal(t, 1, total)
|
||||
})
|
||||
|
||||
t.Run("按ICCID筛选授权记录", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations?iccid=%s&page=1&page_size=20", card1.ICCID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
total := int(data["total"].(float64))
|
||||
assert.Equal(t, 1, total)
|
||||
})
|
||||
|
||||
t.Run("按状态筛选-有效授权", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations?enterprise_id=%d&status=1&page=1&page_size=20", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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 TestAuthorization_GetDetail(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
ts := time.Now().Unix() % 100000
|
||||
shop := env.CreateTestShop("AUTH_TEST_SHOP", 1, nil)
|
||||
|
||||
enterprise := env.CreateTestEnterprise("AUTH_TEST_ENTERPRISE", &shop.ID)
|
||||
|
||||
card1 := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("AC1%d", ts),
|
||||
MSISDN: "13800001001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card1).Error)
|
||||
|
||||
now := time.Now()
|
||||
auth1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card1.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "集成测试授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(auth1).Error)
|
||||
|
||||
t.Run("获取授权记录详情", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d", auth1.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(auth1.ID), data["id"])
|
||||
assert.Equal(t, float64(enterprise.ID), data["enterprise_id"])
|
||||
assert.Equal(t, enterprise.EnterpriseName, data["enterprise_name"])
|
||||
assert.Equal(t, card1.ICCID, data["iccid"])
|
||||
assert.Equal(t, "集成测试授权记录", data["remark"])
|
||||
assert.Equal(t, float64(1), data["status"])
|
||||
})
|
||||
|
||||
t.Run("获取不存在的授权记录", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/authorizations/999999", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorization_UpdateRemark(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
ts := time.Now().Unix() % 100000
|
||||
shop := env.CreateTestShop("AUTH_TEST_SHOP", 1, nil)
|
||||
|
||||
enterprise := env.CreateTestEnterprise("AUTH_TEST_ENTERPRISE", &shop.ID)
|
||||
|
||||
card1 := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("AC1%d", ts),
|
||||
MSISDN: "13800001001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card1).Error)
|
||||
|
||||
now := time.Now()
|
||||
auth1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card1.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "集成测试授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(auth1).Error)
|
||||
|
||||
t.Run("更新授权记录备注", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d/remark", auth1.ID)
|
||||
body := map[string]string{"remark": "更新后的备注内容"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, bodyBytes)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "更新后的备注内容", data["remark"])
|
||||
})
|
||||
|
||||
t.Run("更新不存在的授权记录备注", func(t *testing.T) {
|
||||
body := map[string]string{"remark": "不会更新"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", "/api/admin/authorizations/999999/remark", bodyBytes)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorization_DataPermission(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
ts := time.Now().Unix() % 100000
|
||||
shop := env.CreateTestShop("AUTH_TEST_SHOP", 1, nil)
|
||||
|
||||
enterprise := env.CreateTestEnterprise("AUTH_TEST_ENTERPRISE", &shop.ID)
|
||||
|
||||
card1 := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("AC1%d", ts),
|
||||
MSISDN: "13800001001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card1).Error)
|
||||
|
||||
now := time.Now()
|
||||
auth1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card1.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "集成测试授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(auth1).Error)
|
||||
|
||||
agentAccount := env.CreateTestAccount("agent", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
ts2 := time.Now().Unix() % 100000
|
||||
otherShop := env.CreateTestShop("OTHER_TEST_SHOP", 1, nil)
|
||||
|
||||
otherEnterprise := env.CreateTestEnterprise("OTHER_TEST_ENTERPRISE", &otherShop.ID)
|
||||
|
||||
otherCard := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("OC%d", ts2),
|
||||
MSISDN: "13800002001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &otherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(otherCard).Error)
|
||||
|
||||
otherAuth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: otherEnterprise.ID,
|
||||
CardID: otherCard.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "其他店铺的授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(otherAuth).Error)
|
||||
|
||||
t.Run("代理用户只能看到自己店铺的授权记录", func(t *testing.T) {
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/authorizations?page=1&page_size=100", nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
|
||||
sawOtherAuth := false
|
||||
sawOwnAuth := false
|
||||
for _, item := range items {
|
||||
itemMap := item.(map[string]interface{})
|
||||
authID := uint(itemMap["id"].(float64))
|
||||
if authID == otherAuth.ID {
|
||||
sawOtherAuth = true
|
||||
}
|
||||
if authID == auth1.ID {
|
||||
sawOwnAuth = true
|
||||
}
|
||||
}
|
||||
assert.False(t, sawOtherAuth, "代理用户不应该看到其他店铺的授权记录 (otherAuth.ID=%d)", otherAuth.ID)
|
||||
assert.True(t, sawOwnAuth, "代理用户应该能看到自己店铺的授权记录 (auth1.ID=%d)", auth1.ID)
|
||||
})
|
||||
|
||||
t.Run("平台用户可以看到所有授权记录", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/authorizations?page=1&page_size=100", nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
total := int(data["total"].(float64))
|
||||
assert.GreaterOrEqual(t, total, 2, "平台用户应该能看到所有授权记录")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorization_Unauthorized(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("无Token访问被拒绝", func(t *testing.T) {
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/authorizations", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("无效Token访问被拒绝", func(t *testing.T) {
|
||||
resp, err := env.RequestWithHeaders("GET", "/api/admin/authorizations", nil, map[string]string{
|
||||
"Authorization": "Bearer invalid_token",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorization_UpdateRemarkPermission(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
ts := time.Now().Unix() % 100000
|
||||
shop := env.CreateTestShop("AUTH_PERM_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise("AUTH_PERM_ENTERPRISE", &shop.ID)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("PERM%d", ts),
|
||||
MSISDN: "13800003001",
|
||||
|
||||
Status: 1,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
agentAccount1 := env.CreateTestAccount("agent1", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
agentAccount2 := env.CreateTestAccount("agent2", "password456", constants.UserTypeAgent, &shop.ID, nil)
|
||||
enterpriseAccount := env.CreateTestAccount("enterprise1", "password789", constants.UserTypeEnterprise, nil, &enterprise.ID)
|
||||
|
||||
now := time.Now()
|
||||
authByAgent1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card.ID,
|
||||
AuthorizedBy: agentAccount1.ID,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
Remark: "代理1创建的授权记录",
|
||||
}
|
||||
require.NoError(t, env.TX.Create(authByAgent1).Error)
|
||||
|
||||
t.Run("平台用户可修改任意授权记录备注", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID)
|
||||
body := map[string]string{"remark": "平台修改的备注"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, bodyBytes)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "平台修改的备注", data["remark"])
|
||||
})
|
||||
|
||||
t.Run("代理用户可修改本人创建的授权记录备注", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID)
|
||||
body := map[string]string{"remark": "代理1自己修改的备注"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount1).Request("PUT", url, bodyBytes)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, "代理1自己修改的备注", data["remark"])
|
||||
})
|
||||
|
||||
t.Run("代理用户不可修改他人创建的授权记录备注", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID)
|
||||
body := map[string]string{"remark": "代理2试图修改的备注"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount2).Request("PUT", url, bodyBytes)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
assert.Contains(t, result.Message, "只能修改自己创建的授权记录备注")
|
||||
})
|
||||
|
||||
t.Run("企业用户不允许修改授权记录备注", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/authorizations/%d/remark", authByAgent1.ID)
|
||||
body := map[string]string{"remark": "企业试图修改的备注"}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(enterpriseAccount).Request("PUT", url, bodyBytes)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
assert.Contains(t, result.Message, "权限不足")
|
||||
})
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCarrier_CRUD(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
var createdCarrierID uint
|
||||
|
||||
t.Run("创建运营商", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_code": "TEST_CMCC_001",
|
||||
"carrier_name": "测试中国移动",
|
||||
"carrier_type": constants.CarrierTypeCMCC,
|
||||
"description": "API集成测试创建的运营商",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/carriers", jsonBody)
|
||||
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, "TEST_CMCC_001", dataMap["carrier_code"])
|
||||
assert.Equal(t, "测试中国移动", dataMap["carrier_name"])
|
||||
assert.Equal(t, constants.CarrierTypeCMCC, dataMap["carrier_type"])
|
||||
assert.Equal(t, float64(constants.StatusEnabled), dataMap["status"])
|
||||
|
||||
createdCarrierID = uint(dataMap["id"].(float64))
|
||||
t.Logf("创建的运营商 ID: %d", createdCarrierID)
|
||||
})
|
||||
|
||||
t.Run("创建运营商-编码重复应失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_code": "TEST_CMCC_001",
|
||||
"carrier_name": "重复编码测试",
|
||||
"carrier_type": constants.CarrierTypeCMCC,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/carriers", jsonBody)
|
||||
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, "编码重复应返回错误")
|
||||
})
|
||||
|
||||
t.Run("获取运营商详情", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, "TEST_CMCC_001", dataMap["carrier_code"])
|
||||
})
|
||||
|
||||
t.Run("获取不存在的运营商", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/carriers/99999", nil)
|
||||
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, "不存在的运营商应返回错误")
|
||||
})
|
||||
|
||||
t.Run("更新运营商", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"carrier_name": "测试中国移动-更新",
|
||||
"description": "更新后的描述",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
|
||||
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["carrier_name"])
|
||||
assert.Equal(t, "更新后的描述", dataMap["description"])
|
||||
})
|
||||
|
||||
t.Run("更新运营商状态-禁用", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d/status", createdCarrierID)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
|
||||
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 carrier model.Carrier
|
||||
env.RawDB().First(&carrier, createdCarrierID)
|
||||
assert.Equal(t, constants.StatusDisabled, carrier.Status)
|
||||
})
|
||||
|
||||
t.Run("删除运营商", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/carriers/%d", createdCarrierID)
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
|
||||
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 carrier model.Carrier
|
||||
err = env.RawDB().First(&carrier, createdCarrierID).Error
|
||||
assert.Error(t, err, "删除后应查不到运营商")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCarrier_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
carriers := []*model.Carrier{
|
||||
{CarrierCode: "TEST_LIST_001", CarrierName: "移动列表测试1", CarrierType: constants.CarrierTypeCMCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "TEST_LIST_002", CarrierName: "联通列表测试", CarrierType: constants.CarrierTypeCUCC, Status: constants.StatusEnabled},
|
||||
{CarrierCode: "TEST_LIST_003", CarrierName: "电信列表测试", CarrierType: constants.CarrierTypeCTCC, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, c := range carriers {
|
||||
require.NoError(t, env.TX.Create(c).Error)
|
||||
}
|
||||
carriers[2].Status = constants.StatusDisabled
|
||||
require.NoError(t, env.TX.Save(carriers[2]).Error)
|
||||
|
||||
t.Run("获取运营商列表-无过滤", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/carriers?page=1&page_size=20", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/carriers?carrier_type=CMCC", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/carriers?carrier_name=联通", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/carriers?status=%d", constants.StatusDisabled), nil)
|
||||
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) {
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/carriers", nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGatewayDevice_GetInfo 测试查询设备信息接口
|
||||
func TestGatewayDevice_GetInfo(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000001",
|
||||
DeviceName: "测试设备1",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功查询设备信息", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/devices/by-imei/%s/gateway-info", device.DeviceNo), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺8",
|
||||
ShopCode: "SHOP_008",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000002",
|
||||
DeviceName: "测试设备2",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_1",
|
||||
Phone: "13800000201",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/devices/by-imei/%s/gateway-info", device.DeviceNo), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_GetSlots 测试查询卡槽信息接口
|
||||
func TestGatewayDevice_GetSlots(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000003",
|
||||
DeviceName: "测试设备3",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功查询卡槽信息", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/devices/by-imei/%s/gateway-slots", device.DeviceNo), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺9",
|
||||
ShopCode: "SHOP_009",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000004",
|
||||
DeviceName: "测试设备4",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_2",
|
||||
Phone: "13800000202",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/devices/by-imei/%s/gateway-slots", device.DeviceNo), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_SetSpeedLimit 测试设置限速接口
|
||||
func TestGatewayDevice_SetSpeedLimit(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000005",
|
||||
DeviceName: "测试设备5",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功设置限速", func(t *testing.T) {
|
||||
body := []byte(`{"uploadSpeed": 1024, "downloadSpeed": 2048}`)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/devices/by-imei/%s/speed-limit", device.DeviceNo), body)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺10",
|
||||
ShopCode: "SHOP_010",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000006",
|
||||
DeviceName: "测试设备6",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_3",
|
||||
Phone: "13800000203",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
body := []byte(`{"uploadSpeed": 1024, "downloadSpeed": 2048}`)
|
||||
resp, err := env.AsUser(agentAccount).Request("PUT", fmt.Sprintf("/api/admin/devices/by-imei/%s/speed-limit", device.DeviceNo), body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_SetWiFi 测试设置WiFi接口
|
||||
func TestGatewayDevice_SetWiFi(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000007",
|
||||
DeviceName: "测试设备7",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功设置WiFi", func(t *testing.T) {
|
||||
body := []byte(`{"ssid": "TestWiFi", "password": "password123", "enabled": 1}`)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/devices/by-imei/%s/wifi", device.DeviceNo), body)
|
||||
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("无权限设置其他店铺设备的WiFi", func(t *testing.T) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺11",
|
||||
ShopCode: "SHOP_011",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000008",
|
||||
DeviceName: "测试设备8",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_4",
|
||||
Phone: "13800000204",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
body := []byte(`{"ssid": "TestWiFi", "password": "password123", "enabled": 1}`)
|
||||
resp, err := env.AsUser(agentAccount).Request("PUT", fmt.Sprintf("/api/admin/devices/by-imei/%s/wifi", device.DeviceNo), body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_SwitchCard 测试切卡接口
|
||||
func TestGatewayDevice_SwitchCard(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000009",
|
||||
DeviceName: "测试设备9",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功切卡", func(t *testing.T) {
|
||||
body := []byte(`{"targetIccid": "89860001234567890013"}`)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/switch-card", device.DeviceNo), body)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺12",
|
||||
ShopCode: "SHOP_012",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000010",
|
||||
DeviceName: "测试设备10",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_5",
|
||||
Phone: "13800000205",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
body := []byte(`{"targetIccid": "89860001234567890013"}`)
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/switch-card", device.DeviceNo), body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_RebootDevice 测试重启设备接口
|
||||
func TestGatewayDevice_RebootDevice(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000011",
|
||||
DeviceName: "测试设备11",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功重启设备", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/reboot", device.DeviceNo), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺13",
|
||||
ShopCode: "SHOP_013",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000012",
|
||||
DeviceName: "测试设备12",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_6",
|
||||
Phone: "13800000206",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/reboot", device.DeviceNo), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayDevice_ResetDevice 测试恢复出厂接口
|
||||
func TestGatewayDevice_ResetDevice(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "86000000000000000013",
|
||||
DeviceName: "测试设备13",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("成功恢复出厂", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/reset", device.DeviceNo), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺14",
|
||||
ShopCode: "SHOP_014",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
device2 := &model.Device{
|
||||
DeviceNo: "86000000000000000014",
|
||||
DeviceName: "测试设备14",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_device_gateway_7",
|
||||
Phone: "13800000207",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", fmt.Sprintf("/api/admin/devices/by-imei/%s/reset", device.DeviceNo), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -1,502 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDevice_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试设备
|
||||
devices := []*model.Device{
|
||||
{DeviceNo: "TEST_DEVICE_001", DeviceName: "测试设备1", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock},
|
||||
{DeviceNo: "TEST_DEVICE_002", DeviceName: "测试设备2", DeviceType: "router", MaxSimSlots: 2, Status: constants.DeviceStatusInStock},
|
||||
{DeviceNo: "TEST_DEVICE_003", DeviceName: "测试设备3", DeviceType: "mifi", MaxSimSlots: 1, Status: constants.DeviceStatusDistributed},
|
||||
}
|
||||
for _, device := range devices {
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
}
|
||||
|
||||
t.Run("获取设备列表-无过滤", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices?page=1&page_size=20", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices?device_type=router", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/devices?status=%d", constants.DeviceStatusInStock), nil)
|
||||
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) {
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/devices", nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDevice_GetByID(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试设备
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_DEVICE_GET_001",
|
||||
DeviceName: "测试设备详情",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("获取设备详情-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, "TEST_DEVICE_GET_001", dataMap["device_no"])
|
||||
})
|
||||
|
||||
t.Run("获取不存在的设备-应返回错误", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/999999", nil)
|
||||
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, "不存在的设备应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDevice_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_DEVICE_DEL_001",
|
||||
DeviceName: "测试删除设备",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("删除设备-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/%d", device.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
|
||||
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 deletedDevice model.Device
|
||||
err = env.RawDB().Unscoped().First(&deletedDevice, device.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, deletedDevice.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeviceImport_TaskList(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
task := &model.DeviceImportTask{
|
||||
TaskNo: "TEST_DEVICE_IMPORT_001",
|
||||
Status: model.ImportTaskStatusCompleted,
|
||||
BatchNo: "TEST_BATCH_001",
|
||||
TotalCount: 100,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(task).Error)
|
||||
|
||||
t.Run("获取导入任务列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/import/tasks?page=1&page_size=20", nil)
|
||||
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) {
|
||||
url := fmt.Sprintf("/api/admin/devices/import/tasks/%d", task.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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 TestDevice_GetByIMEI(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试设备
|
||||
device := &model.Device{
|
||||
DeviceNo: "TEST_IMEI_001",
|
||||
DeviceName: "测试IMEI查询设备",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
t.Run("通过IMEI查询设备详情-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, "TEST_IMEI_001", dataMap["device_no"])
|
||||
assert.Equal(t, "测试IMEI查询设备", dataMap["device_name"])
|
||||
})
|
||||
|
||||
t.Run("通过不存在的IMEI查询-应返回错误", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/devices/by-imei/NONEXISTENT_IMEI", nil)
|
||||
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, "不存在的IMEI应返回错误码")
|
||||
})
|
||||
|
||||
t.Run("未认证请求-应返回错误", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/devices/by-imei/%s", device.DeviceNo)
|
||||
resp, err := env.ClearAuth().Request("GET", url, nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestDevice_BatchSetSeriesBinding(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_dev_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
series := createTestPackageSeries(t, env, "测试系列")
|
||||
createTestAllocation(t, env, shop.ID, series.ID, 0)
|
||||
|
||||
devices := []*model.Device{
|
||||
{DeviceNo: fmt.Sprintf("DEV_%d_001", time.Now().UnixNano()), DeviceName: "测试设备1", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
|
||||
{DeviceNo: fmt.Sprintf("DEV_%d_002", time.Now().UnixNano()), DeviceName: "测试设备2", DeviceType: "mifi", MaxSimSlots: 2, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
|
||||
{DeviceNo: fmt.Sprintf("DEV_%d_003", time.Now().UnixNano()), DeviceName: "测试设备3", DeviceType: "router", MaxSimSlots: 4, Status: constants.DeviceStatusInStock, ShopID: &shop.ID},
|
||||
}
|
||||
for _, device := range devices {
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
}
|
||||
|
||||
t.Run("批量设置设备系列绑定-成功", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[0].ID, devices[1].ID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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, "应返回成功: %s", result.Message)
|
||||
|
||||
if result.Data != nil {
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(2), dataMap["success_count"], "应有2个设备成功绑定")
|
||||
assert.Equal(t, float64(0), dataMap["fail_count"], "应无失败")
|
||||
} else {
|
||||
t.Logf("Response data is nil: code=%d, message=%s", result.Code, result.Message)
|
||||
}
|
||||
|
||||
var updatedDevice model.Device
|
||||
err = env.RawDB().Where("id = ?", devices[0].ID).First(&updatedDevice).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, updatedDevice.SeriesID)
|
||||
assert.Equal(t, series.ID, *updatedDevice.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("清除设备系列绑定-series_id=0", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[0].ID},
|
||||
"series_id": 0,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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 updatedDevice model.Device
|
||||
err = env.RawDB().Where("id = ?", devices[0].ID).First(&updatedDevice).Error
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, updatedDevice.SeriesID, "系列分配应被清除")
|
||||
})
|
||||
|
||||
t.Run("批量设置-部分设备不存在", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[2].ID, 999999},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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)
|
||||
|
||||
if result.Data != nil {
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), dataMap["success_count"], "应有1个设备成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应有1个设备失败")
|
||||
|
||||
if dataMap["failed_items"] != nil {
|
||||
failedItems := dataMap["failed_items"].([]interface{})
|
||||
assert.Len(t, failedItems, 1)
|
||||
failedItem := failedItems[0].(map[string]interface{})
|
||||
assert.Equal(t, float64(999999), failedItem["device_id"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("设置不存在的系列分配-应失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[2].ID},
|
||||
"series_id": 999999,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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, "不存在的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("设置禁用的系列-应失败", func(t *testing.T) {
|
||||
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
|
||||
env.TX.Model(&model.PackageSeries{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[2].ID},
|
||||
"series_id": disabledSeries.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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, "禁用的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("代理商设置其他店铺的设备-应失败", func(t *testing.T) {
|
||||
otherShop := env.CreateTestShop("其他店铺", 1, nil)
|
||||
otherDevice := &model.Device{
|
||||
DeviceNo: fmt.Sprintf("OTHER_%d", time.Now().UnixNano()),
|
||||
DeviceName: "其他设备",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &otherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(otherDevice).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{otherDevice.ID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), dataMap["success_count"], "不应有成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应全部失败")
|
||||
})
|
||||
|
||||
t.Run("超级管理员可以设置任意店铺的设备", func(t *testing.T) {
|
||||
anotherShop := env.CreateTestShop("另一个店铺", 1, nil)
|
||||
anotherDevice := &model.Device{
|
||||
DeviceNo: fmt.Sprintf("ADMIN_%d", time.Now().UnixNano()),
|
||||
DeviceName: "管理员设备",
|
||||
DeviceType: "router",
|
||||
MaxSimSlots: 4,
|
||||
Status: constants.DeviceStatusInStock,
|
||||
ShopID: &anotherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(anotherDevice).Error)
|
||||
|
||||
createTestAllocation(t, env, anotherShop.ID, series.ID, 0)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{anotherDevice.ID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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, float64(1), dataMap["success_count"])
|
||||
})
|
||||
|
||||
t.Run("未认证请求应返回错误", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{devices[0].ID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.ClearAuth().Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
|
||||
t.Run("空设备ID列表-返回成功但无操作", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"device_ids": []uint{},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/devices/series-binding", jsonBody)
|
||||
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["success_count"], "空列表无成功项")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uniqueEDH5TestPrefix() string {
|
||||
return fmt.Sprintf("H5ED%d", time.Now().UnixNano()%1000000000)
|
||||
}
|
||||
|
||||
func TestEnterpriseDeviceH5_ListDevices(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDH5TestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
enterpriseUser := env.CreateTestAccount(prefix+"_USER", "Password123", constants.UserTypeEnterprise, nil, &enterprise.ID)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
now := time.Now()
|
||||
deviceAuth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(deviceAuth).Error)
|
||||
|
||||
t.Run("企业用户获取授权设备列表", func(t *testing.T) {
|
||||
resp, err := env.AsUser(enterpriseUser).Request("GET", "/api/h5/devices?page=1&page_size=10", nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), data["total"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseDeviceH5_GetDeviceDetail(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDH5TestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
enterpriseUser := env.CreateTestAccount(prefix+"_USER", "Password123", constants.UserTypeEnterprise, nil, &enterprise.ID)
|
||||
|
||||
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
card := &model.IotCard{ICCID: prefix + "0001", CarrierID: carrier.ID, Status: 2, ShopID: &shop.ID, NetworkStatus: 1}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
now := time.Now()
|
||||
binding := &model.DeviceSimBinding{DeviceID: device.ID, IotCardID: card.ID, SlotPosition: 1, BindStatus: 1, BindTime: &now}
|
||||
require.NoError(t, env.TX.Create(binding).Error)
|
||||
|
||||
deviceAuth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(deviceAuth).Error)
|
||||
|
||||
cardAuth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card.ID,
|
||||
DeviceAuthID: &deviceAuth.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(cardAuth).Error)
|
||||
|
||||
t.Run("成功获取设备详情", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/h5/devices/%d", device.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
deviceInfo := data["device"].(map[string]interface{})
|
||||
assert.Equal(t, float64(device.ID), deviceInfo["device_id"])
|
||||
assert.Equal(t, device.DeviceNo, deviceInfo["device_no"])
|
||||
|
||||
cards := data["cards"].([]interface{})
|
||||
assert.Len(t, cards, 1)
|
||||
})
|
||||
|
||||
t.Run("设备未授权返回错误", func(t *testing.T) {
|
||||
device2 := &model.Device{
|
||||
DeviceNo: prefix + "_D002",
|
||||
DeviceName: "未授权设备",
|
||||
Status: 2,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/h5/devices/%d", device2.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("GET", url, nil)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseDeviceH5_SuspendCard(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDH5TestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
enterpriseUser := env.CreateTestAccount(prefix+"_USER", "Password123", constants.UserTypeEnterprise, nil, &enterprise.ID)
|
||||
|
||||
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
card := &model.IotCard{ICCID: prefix + "0001", CarrierID: carrier.ID, Status: 2, ShopID: &shop.ID, NetworkStatus: 1}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
now := time.Now()
|
||||
binding := &model.DeviceSimBinding{DeviceID: device.ID, IotCardID: card.ID, SlotPosition: 1, BindStatus: 1, BindTime: &now}
|
||||
require.NoError(t, env.TX.Create(binding).Error)
|
||||
|
||||
deviceAuth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(deviceAuth).Error)
|
||||
|
||||
cardAuth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card.ID,
|
||||
DeviceAuthID: &deviceAuth.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(cardAuth).Error)
|
||||
|
||||
t.Run("成功停机", func(t *testing.T) {
|
||||
reqBody := dto.DeviceCardOperationReq{Reason: "测试停机"}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/h5/devices/%d/cards/%d/suspend", device.ID, card.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["success"])
|
||||
})
|
||||
|
||||
t.Run("卡不属于设备返回错误", func(t *testing.T) {
|
||||
card2 := &model.IotCard{ICCID: prefix + "0002", CarrierID: carrier.ID, Status: 2}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
reqBody := dto.DeviceCardOperationReq{Reason: "测试停机"}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/h5/devices/%d/cards/%d/suspend", device.ID, card2.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("POST", url, body)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseDeviceH5_ResumeCard(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDH5TestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
enterpriseUser := env.CreateTestAccount(prefix+"_USER", "Password123", constants.UserTypeEnterprise, nil, &enterprise.ID)
|
||||
|
||||
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
card := &model.IotCard{ICCID: prefix + "0001", CarrierID: carrier.ID, Status: 2, ShopID: &shop.ID, NetworkStatus: 0}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
now := time.Now()
|
||||
binding := &model.DeviceSimBinding{DeviceID: device.ID, IotCardID: card.ID, SlotPosition: 1, BindStatus: 1, BindTime: &now}
|
||||
require.NoError(t, env.TX.Create(binding).Error)
|
||||
|
||||
deviceAuth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(deviceAuth).Error)
|
||||
|
||||
cardAuth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
CardID: card.ID,
|
||||
DeviceAuthID: &deviceAuth.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(cardAuth).Error)
|
||||
|
||||
t.Run("成功复机", func(t *testing.T) {
|
||||
reqBody := dto.DeviceCardOperationReq{Reason: "测试复机"}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/h5/devices/%d/cards/%d/resume", device.ID, card.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, true, data["success"])
|
||||
})
|
||||
|
||||
t.Run("设备未授权返回错误", func(t *testing.T) {
|
||||
device2 := &model.Device{
|
||||
DeviceNo: prefix + "_D002",
|
||||
DeviceName: "未授权设备",
|
||||
Status: 2,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
reqBody := dto.DeviceCardOperationReq{Reason: "测试复机"}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/h5/devices/%d/cards/%d/resume", device2.ID, card.ID)
|
||||
resp, err := env.AsUser(enterpriseUser).Request("POST", url, body)
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uniqueEDTestPrefix() string {
|
||||
return fmt.Sprintf("ED%d", time.Now().UnixNano()%1000000000)
|
||||
}
|
||||
|
||||
func TestEnterpriseDevice_AllocateDevices(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDTestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
|
||||
device1 := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备1",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
device2 := &model.Device{
|
||||
DeviceNo: prefix + "_D002",
|
||||
DeviceName: "测试设备2",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device1).Error)
|
||||
require.NoError(t, env.TX.Create(device2).Error)
|
||||
|
||||
carrier := &model.Carrier{CarrierName: "测试运营商", CarrierType: "CMCC", Status: 1}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
card := &model.IotCard{ICCID: prefix + "0001", CarrierID: carrier.ID, Status: 2, ShopID: &shop.ID}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
now := time.Now()
|
||||
binding := &model.DeviceSimBinding{DeviceID: device1.ID, IotCardID: card.ID, SlotPosition: 1, BindStatus: 1, BindTime: &now}
|
||||
require.NoError(t, env.TX.Create(binding).Error)
|
||||
|
||||
t.Run("成功授权设备给企业", func(t *testing.T) {
|
||||
reqBody := dto.AllocateDevicesReq{
|
||||
DeviceNos: []string{device1.DeviceNo},
|
||||
Remark: "集成测试授权",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/allocate-devices", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), data["success_count"])
|
||||
assert.Equal(t, float64(0), data["fail_count"])
|
||||
})
|
||||
|
||||
t.Run("设备不存在时记录失败", func(t *testing.T) {
|
||||
reqBody := dto.AllocateDevicesReq{
|
||||
DeviceNos: []string{"NOT_EXIST_DEVICE"},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/allocate-devices", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), data["success_count"])
|
||||
assert.Equal(t, float64(1), data["fail_count"])
|
||||
})
|
||||
|
||||
t.Run("企业不存在返回错误", func(t *testing.T) {
|
||||
reqBody := dto.AllocateDevicesReq{
|
||||
DeviceNos: []string{device2.DeviceNo},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/enterprises/99999/allocate-devices", body)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseDevice_RecallDevices(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDTestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
|
||||
device := &model.Device{
|
||||
DeviceNo: prefix + "_D001",
|
||||
DeviceName: "测试设备",
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(device).Error)
|
||||
|
||||
now := time.Now()
|
||||
deviceAuth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(deviceAuth).Error)
|
||||
|
||||
t.Run("成功撤销设备授权", func(t *testing.T) {
|
||||
reqBody := dto.RecallDevicesReq{
|
||||
DeviceNos: []string{device.DeviceNo},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/recall-devices", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(1), data["success_count"])
|
||||
})
|
||||
|
||||
t.Run("设备未授权时返回失败", func(t *testing.T) {
|
||||
reqBody := dto.RecallDevicesReq{
|
||||
DeviceNos: []string{prefix + "_D002"},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/recall-devices", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", url, body)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), data["success_count"])
|
||||
assert.Equal(t, float64(1), data["fail_count"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseDevice_ListDevices(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
prefix := uniqueEDTestPrefix()
|
||||
shop := env.CreateTestShop(prefix+"_SHOP", 1, nil)
|
||||
enterprise := env.CreateTestEnterprise(prefix+"_ENTERPRISE", &shop.ID)
|
||||
|
||||
devices := make([]*model.Device, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
devices[i] = &model.Device{
|
||||
DeviceNo: fmt.Sprintf("%s_D%03d", prefix, i+1),
|
||||
DeviceName: fmt.Sprintf("测试设备%d", i+1),
|
||||
Status: 2,
|
||||
ShopID: &shop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(devices[i]).Error)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, device := range devices[:2] {
|
||||
auth := &model.EnterpriseDeviceAuthorization{
|
||||
EnterpriseID: enterprise.ID,
|
||||
DeviceID: device.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(auth).Error)
|
||||
}
|
||||
|
||||
t.Run("获取企业授权设备列表", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/devices?page=1&page_size=10", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(2), data["total"])
|
||||
})
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/enterprises/%d/devices?page=1&page_size=1", enterprise.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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)
|
||||
|
||||
data := result.Data.(map[string]interface{})
|
||||
items := data["items"].([]interface{})
|
||||
assert.Len(t, items, 1)
|
||||
})
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestErrorCodeValidation_PackageNotFound(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("套餐不存在返回404", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/packages/99999", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证 HTTP 状态码
|
||||
assert.Equal(t, 404, resp.StatusCode, "应返回 404 Not Found")
|
||||
|
||||
// 验证错误码
|
||||
code, ok := result["code"].(float64)
|
||||
require.True(t, ok, "响应应包含 code 字段")
|
||||
assert.Equal(t, float64(errors.CodeNotFound), code, "应返回 CodeNotFound")
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorCodeValidation_InsufficientBalance(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("余额不足返回400", func(t *testing.T) {
|
||||
// 创建测试店铺和提现申请
|
||||
// 这里需要先创建一个店铺,然后申请提现金额 > 余额
|
||||
// 由于涉及较多前置步骤,这里仅验证错误码映射正确性
|
||||
|
||||
// 假设有一个提现接口,提现金额大于余额
|
||||
body := []byte(`{"amount": 1000000000}`) // 10亿分,肯定超出余额
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/commission_withdrawals", body)
|
||||
|
||||
// 如果接口不存在或需要特定条件,跳过此测试
|
||||
if err != nil || resp.StatusCode == 404 {
|
||||
t.Skip("提现接口需要特定前置条件,跳过测试")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 如果成功请求,验证错误码
|
||||
if resp.StatusCode != 200 {
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
code, ok := result["code"].(float64)
|
||||
if ok && code == float64(errors.CodeInsufficientBalance) {
|
||||
assert.Equal(t, 400, resp.StatusCode, "余额不足应返回 400")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorCodeValidation_ShopCodeDuplicate(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("店铺代码重复返回409", func(t *testing.T) {
|
||||
// 创建第一个店铺
|
||||
shopCode := fmt.Sprintf("TEST_SHOP_%d", time.Now().UnixNano())
|
||||
body1 := fmt.Sprintf(`{
|
||||
"shop_name": "测试店铺1",
|
||||
"shop_code": "%s",
|
||||
"level": 1,
|
||||
"contact_name": "联系人1",
|
||||
"contact_phone": "13800138001",
|
||||
"status": 1
|
||||
}`, shopCode)
|
||||
|
||||
resp1, err := env.AsSuperAdmin().Request("POST", "/api/admin/shops", []byte(body1))
|
||||
require.NoError(t, err)
|
||||
defer resp1.Body.Close()
|
||||
|
||||
if resp1.StatusCode != 200 {
|
||||
t.Skipf("创建店铺失败,状态码: %d", resp1.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试创建重复店铺代码
|
||||
body2 := fmt.Sprintf(`{
|
||||
"shop_name": "测试店铺2",
|
||||
"shop_code": "%s",
|
||||
"level": 1,
|
||||
"contact_name": "联系人2",
|
||||
"contact_phone": "13800138002",
|
||||
"status": 1
|
||||
}`, shopCode)
|
||||
|
||||
resp2, err := env.AsSuperAdmin().Request("POST", "/api/admin/shops", []byte(body2))
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
err = json.NewDecoder(resp2.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证 HTTP 状态码
|
||||
assert.Equal(t, 409, resp2.StatusCode, "重复店铺代码应返回 409 Conflict")
|
||||
|
||||
// 验证错误码
|
||||
code, ok := result["code"].(float64)
|
||||
require.True(t, ok, "响应应包含 code 字段")
|
||||
assert.Equal(t, float64(errors.CodeShopCodeExists), code, "应返回 CodeShopCodeExists")
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorCodeValidation_LogLevels(t *testing.T) {
|
||||
t.Run("验证日志级别配置", func(t *testing.T) {
|
||||
// 4xx 错误应该是 WARN 级别
|
||||
// 5xx 错误应该是 ERROR 级别
|
||||
// 这个在 pkg/errors/handler.go 中已经实现
|
||||
|
||||
// 验证错误码的 HTTP 状态码映射
|
||||
testCases := []struct {
|
||||
code int
|
||||
expectedStatus int
|
||||
expectedLevel string
|
||||
}{
|
||||
{errors.CodeNotFound, 404, "WARN"},
|
||||
{errors.CodeInvalidParam, 400, "WARN"},
|
||||
{errors.CodeShopCodeExists, 409, "WARN"},
|
||||
{errors.CodeInsufficientBalance, 400, "WARN"},
|
||||
{errors.CodeInternalError, 500, "ERROR"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
httpStatus := errors.GetHTTPStatus(tc.code)
|
||||
assert.Equal(t, tc.expectedStatus, httpStatus,
|
||||
"错误码 %d 应映射到 HTTP %d", tc.code, tc.expectedStatus)
|
||||
|
||||
// 验证日志级别(4xx -> WARN, 5xx -> ERROR)
|
||||
expectedLevel := "WARN"
|
||||
if httpStatus >= 500 {
|
||||
expectedLevel = "ERROR"
|
||||
}
|
||||
assert.Equal(t, expectedLevel, tc.expectedLevel,
|
||||
"HTTP %d 应使用 %s 级别日志", httpStatus, expectedLevel)
|
||||
}
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,368 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestGatewayCard_GetStatus 测试查询卡状态接口
|
||||
func TestGatewayCard_GetStatus(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890001",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功查询卡状态", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-status", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺2",
|
||||
ShopCode: "SHOP_002",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890002",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_1",
|
||||
Phone: "13800000101",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-status", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayCard_GetFlow 测试查询流量接口
|
||||
func TestGatewayCard_GetFlow(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890003",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功查询流量使用", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-flow", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺3",
|
||||
ShopCode: "SHOP_003",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890004",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_2",
|
||||
Phone: "13800000102",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-flow", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayCard_GetRealname 测试查询实名状态接口
|
||||
func TestGatewayCard_GetRealname(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890005",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功查询实名状态", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-realname", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺4",
|
||||
ShopCode: "SHOP_004",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890006",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_3",
|
||||
Phone: "13800000103",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/gateway-realname", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayCard_GetRealnameLink 测试获取实名链接接口
|
||||
func TestGatewayCard_GetRealnameLink(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890007",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功获取实名链接", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/realname-link", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺5",
|
||||
ShopCode: "SHOP_005",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890008",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_4",
|
||||
Phone: "13800000104",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", fmt.Sprintf("/api/admin/iot-cards/%s/realname-link", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayCard_StopCard 测试停机接口
|
||||
func TestGatewayCard_StopCard(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890009",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功停机", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/iot-cards/%s/stop", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺6",
|
||||
ShopCode: "SHOP_006",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890010",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_5",
|
||||
Phone: "13800000105",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", fmt.Sprintf("/api/admin/iot-cards/%s/stop", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGatewayCard_StartCard 测试复机接口
|
||||
func TestGatewayCard_StartCard(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "89860001234567890011",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("成功复机", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/iot-cards/%s/start", card.ICCID), nil)
|
||||
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) {
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "测试店铺7",
|
||||
ShopCode: "SHOP_007",
|
||||
Level: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(shop2).Error)
|
||||
|
||||
card2 := &model.IotCard{
|
||||
ICCID: "89860001234567890012",
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &shop2.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card2).Error)
|
||||
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_test_gateway_6",
|
||||
Phone: "13800000106",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shop2.ID,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(agentAccount).Error)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", fmt.Sprintf("/api/admin/iot-cards/%s/start", card.ICCID), nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 404, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -1,861 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgerrors "github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestIotCard_ListStandalone(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "TEST0012345678901001", CarrierID: 1, Status: 1},
|
||||
{ICCID: "TEST0012345678901002", CarrierID: 1, Status: 1},
|
||||
{ICCID: "TEST0012345678901003", CarrierID: 2, Status: 2},
|
||||
}
|
||||
for _, card := range cards {
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
}
|
||||
|
||||
t.Run("获取单卡列表-无过滤", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?page=1&page_size=20", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?carrier_id=1", nil)
|
||||
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("获取单卡列表-按ICCID模糊查询", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?iccid=901001", nil)
|
||||
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) {
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/iot-cards/standalone", nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_Import(t *testing.T) {
|
||||
t.Skip("E2E测试:需要 Worker 服务运行处理异步导入任务")
|
||||
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("导入CSV文件", func(t *testing.T) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "test.csv")
|
||||
require.NoError(t, err)
|
||||
csvContent := "iccid\nTEST0012345678902001\nTEST0012345678902002\nTEST0012345678902003"
|
||||
_, err = part.Write([]byte(csvContent))
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = writer.WriteField("carrier_id", "1")
|
||||
_ = writer.WriteField("carrier_type", "CMCC")
|
||||
_ = writer.WriteField("batch_no", "TEST_BATCH_001")
|
||||
writer.Close()
|
||||
|
||||
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
})
|
||||
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("Import response: code=%d, message=%s", result.Code, result.Message)
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
|
||||
t.Run("导入无文件应返回错误", func(t *testing.T) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
_ = writer.WriteField("carrier_id", "1")
|
||||
_ = writer.WriteField("carrier_type", "CMCC")
|
||||
writer.Close()
|
||||
|
||||
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
})
|
||||
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("No file response: code=%d, message=%s, data=%v", result.Code, result.Message, result.Data)
|
||||
assert.NotEqual(t, 0, result.Code, "无文件时应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_ImportTaskList(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
task := &model.IotCardImportTask{
|
||||
TaskNo: "TEST20260123001",
|
||||
Status: model.ImportTaskStatusCompleted,
|
||||
CarrierID: 1,
|
||||
CarrierType: "CMCC",
|
||||
CarrierName: "中国移动",
|
||||
TotalCount: 100,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(task).Error)
|
||||
|
||||
t.Run("获取导入任务列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/import-tasks?page=1&page_size=20", nil)
|
||||
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) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/import-tasks/%d", task.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, "CMCC", dataMap["carrier_type"], "任务详情应返回冗余的运营商类型")
|
||||
assert.Equal(t, "中国移动", dataMap["carrier_name"], "任务详情应返回冗余的运营商名称")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_ImportTask_PlatformOnly(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("权限测试店铺", 1, nil)
|
||||
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_perm_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
task := &model.IotCardImportTask{
|
||||
TaskNo: fmt.Sprintf("TEST_PERM_%d", time.Now().UnixNano()),
|
||||
Status: model.ImportTaskStatusCompleted,
|
||||
CarrierID: 1,
|
||||
CarrierType: "CMCC",
|
||||
CarrierName: "中国移动",
|
||||
TotalCount: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(task).Error)
|
||||
|
||||
t.Run("代理账号提交导入任务应返回403", func(t *testing.T) {
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"carrier_id": 1,
|
||||
"batch_no": "TEST_BATCH_PERM",
|
||||
"file_key": "imports/test.xlsx",
|
||||
})
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/iot-cards/import", body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pkgerrors.CodeForbidden, result.Code)
|
||||
assert.Contains(t, result.Message, "仅平台用户")
|
||||
})
|
||||
|
||||
t.Run("代理账号访问导入任务列表应返回403", func(t *testing.T) {
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", "/api/admin/iot-cards/import-tasks?page=1&page_size=20", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pkgerrors.CodeForbidden, result.Code)
|
||||
assert.Contains(t, result.Message, "仅平台用户")
|
||||
})
|
||||
|
||||
t.Run("代理账号访问导入任务详情应返回403", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/import-tasks/%d", task.ID)
|
||||
resp, err := env.AsUser(agentAccount).Request("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 403, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pkgerrors.CodeForbidden, result.Code)
|
||||
assert.Contains(t, result.Message, "仅平台用户")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_ImportE2E(t *testing.T) {
|
||||
t.Skip("E2E测试:需要 Worker 服务运行处理异步导入任务")
|
||||
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 准备测试用的 ICCID(20位,满足 CMCC 要求)
|
||||
testICCIDPrefix := "E2ETEST"
|
||||
testBatchNo1 := fmt.Sprintf("E2E_BATCH_%d_001", time.Now().UnixNano())
|
||||
testBatchNo2 := fmt.Sprintf("E2E_BATCH_%d_002", time.Now().UnixNano())
|
||||
testICCIDs := []string{
|
||||
testICCIDPrefix + "1234567890123",
|
||||
testICCIDPrefix + "1234567890124",
|
||||
testICCIDPrefix + "1234567890125",
|
||||
}
|
||||
|
||||
t.Run("完整导入流程验证", func(t *testing.T) {
|
||||
// Step 1: 通过 API 提交导入任务
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "e2e_test.csv")
|
||||
require.NoError(t, err)
|
||||
csvContent := "iccid\n" + testICCIDs[0] + "\n" + testICCIDs[1] + "\n" + testICCIDs[2]
|
||||
_, err = part.Write([]byte(csvContent))
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = writer.WriteField("carrier_id", "1")
|
||||
_ = writer.WriteField("carrier_type", "CMCC")
|
||||
_ = writer.WriteField("batch_no", testBatchNo1)
|
||||
writer.Close()
|
||||
|
||||
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResult response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&apiResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, apiResult.Code, "API 应返回成功: %s", apiResult.Message)
|
||||
|
||||
// 从响应中提取 task_id
|
||||
dataMap, ok := apiResult.Data.(map[string]interface{})
|
||||
require.True(t, ok, "响应数据应为 map")
|
||||
taskIDFloat, ok := dataMap["task_id"].(float64)
|
||||
require.True(t, ok, "task_id 应存在")
|
||||
taskID := uint(taskIDFloat)
|
||||
t.Logf("创建的导入任务 ID: %d", taskID)
|
||||
|
||||
// Step 2: 等待 Worker 处理完成(轮询检查任务状态)
|
||||
var importTask model.IotCardImportTask
|
||||
maxWaitTime := 30 * time.Second
|
||||
pollInterval := 500 * time.Millisecond
|
||||
startTime := time.Now()
|
||||
|
||||
ctx := context.Background()
|
||||
skipCtx := pkggorm.SkipDataPermission(ctx)
|
||||
for {
|
||||
if time.Since(startTime) > maxWaitTime {
|
||||
t.Fatalf("等待超时:任务 %d 未在 %v 内完成", taskID, maxWaitTime)
|
||||
}
|
||||
|
||||
err = env.RawDB().WithContext(skipCtx).First(&importTask, taskID).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("任务状态: %d (1=pending, 2=processing, 3=completed, 4=failed)", importTask.Status)
|
||||
|
||||
if importTask.Status == model.ImportTaskStatusCompleted || importTask.Status == model.ImportTaskStatusFailed {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(pollInterval)
|
||||
}
|
||||
|
||||
// Step 3: 验证任务完成状态
|
||||
assert.Equal(t, model.ImportTaskStatusCompleted, importTask.Status, "任务应完成")
|
||||
assert.Equal(t, 3, importTask.TotalCount, "总数应为3")
|
||||
assert.Equal(t, 3, importTask.SuccessCount, "成功数应为3")
|
||||
assert.Equal(t, 0, importTask.SkipCount, "跳过数应为0")
|
||||
assert.Equal(t, 0, importTask.FailCount, "失败数应为0")
|
||||
t.Logf("任务完成: total=%d, success=%d, skip=%d, fail=%d",
|
||||
importTask.TotalCount, importTask.SuccessCount, importTask.SkipCount, importTask.FailCount)
|
||||
|
||||
// Step 4: 验证 IoT 卡已入库
|
||||
var cards []model.IotCard
|
||||
err = env.RawDB().WithContext(skipCtx).Where("iccid IN ?", testICCIDs).Find(&cards).Error
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cards, 3, "应创建3张 IoT 卡")
|
||||
|
||||
for _, card := range cards {
|
||||
assert.Equal(t, uint(1), card.CarrierID, "运营商ID应为1")
|
||||
assert.Equal(t, testBatchNo1, card.BatchNo, "批次号应匹配")
|
||||
assert.Equal(t, 1, card.Status, "状态应为在库(1)")
|
||||
t.Logf("已创建 IoT 卡: ICCID=%s, ID=%d", card.ICCID, card.ID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("重复导入应跳过已存在的ICCID", func(t *testing.T) {
|
||||
// 再次导入相同的 ICCID,应该全部跳过
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", "e2e_test_dup.csv")
|
||||
require.NoError(t, err)
|
||||
csvContent := "iccid\n" + testICCIDs[0] + "\n" + testICCIDs[1]
|
||||
_, err = part.Write([]byte(csvContent))
|
||||
require.NoError(t, err)
|
||||
|
||||
_ = writer.WriteField("carrier_id", "1")
|
||||
_ = writer.WriteField("carrier_type", "CMCC")
|
||||
_ = writer.WriteField("batch_no", testBatchNo2)
|
||||
writer.Close()
|
||||
|
||||
resp, err := env.AsSuperAdmin().RequestWithHeaders("POST", "/api/admin/iot-cards/import", body.Bytes(), map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var apiResult response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&apiResult)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, apiResult.Code)
|
||||
|
||||
dataMap := apiResult.Data.(map[string]interface{})
|
||||
taskID := uint(dataMap["task_id"].(float64))
|
||||
|
||||
// 等待处理完成
|
||||
var importTask model.IotCardImportTask
|
||||
maxWaitTime := 30 * time.Second
|
||||
startTime := time.Now()
|
||||
ctx := context.Background()
|
||||
skipCtx := pkggorm.SkipDataPermission(ctx)
|
||||
|
||||
for {
|
||||
if time.Since(startTime) > maxWaitTime {
|
||||
t.Fatalf("等待超时")
|
||||
}
|
||||
env.RawDB().WithContext(skipCtx).First(&importTask, taskID)
|
||||
if importTask.Status == model.ImportTaskStatusCompleted || importTask.Status == model.ImportTaskStatusFailed {
|
||||
break
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 验证:2条应该全部跳过
|
||||
assert.Equal(t, model.ImportTaskStatusCompleted, importTask.Status)
|
||||
assert.Equal(t, 2, importTask.TotalCount)
|
||||
assert.Equal(t, 0, importTask.SuccessCount, "成功数应为0(全部跳过)")
|
||||
assert.Equal(t, 2, importTask.SkipCount, "跳过数应为2")
|
||||
t.Logf("重复导入结果: success=%d, skip=%d", importTask.SuccessCount, importTask.SkipCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_CarrierRedundantFields(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
carrierCode := fmt.Sprintf("REDUND_%d", time.Now().UnixNano())
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: carrierCode,
|
||||
CarrierName: "冗余字段测试运营商",
|
||||
CarrierType: "CUCC",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
|
||||
card := &model.IotCard{
|
||||
ICCID: testICCID,
|
||||
CarrierID: carrier.ID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("单卡列表应返回冗余字段", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/standalone?iccid="+testICCID, nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, result.Code, "API应返回成功,实际: %v", result.Message)
|
||||
|
||||
dataMap, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok, "Data应为map类型,实际: %T", result.Data)
|
||||
items, ok := dataMap["items"].([]interface{})
|
||||
require.True(t, ok, "items字段应存在且为数组,dataMap: %+v", dataMap)
|
||||
require.GreaterOrEqual(t, len(items), 1, "列表应至少有1条记录,ICCID: %s", testICCID)
|
||||
|
||||
cardData := items[0].(map[string]interface{})
|
||||
assert.Equal(t, "CUCC", cardData["carrier_type"], "列表应返回冗余的运营商类型")
|
||||
assert.Equal(t, "冗余字段测试运营商", cardData["carrier_name"], "列表应返回冗余的运营商名称")
|
||||
})
|
||||
|
||||
t.Run("单卡详情应返回冗余字段", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, "CUCC", dataMap["carrier_type"], "详情应返回冗余的运营商类型")
|
||||
assert.Equal(t, "冗余字段测试运营商", dataMap["carrier_name"], "详情应返回冗余的运营商名称")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_GetByICCID(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
carrierCode := fmt.Sprintf("ICCID_%d", time.Now().UnixNano())
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: carrierCode,
|
||||
CarrierName: "测试运营商",
|
||||
CarrierType: "CMCC",
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(carrier).Error)
|
||||
|
||||
testICCID := fmt.Sprintf("8986%016d", time.Now().UnixNano()%10000000000000000)
|
||||
card := &model.IotCard{
|
||||
ICCID: testICCID,
|
||||
CarrierID: carrier.ID,
|
||||
CarrierType: carrier.CarrierType,
|
||||
CarrierName: carrier.CarrierName,
|
||||
MSISDN: "13800000001",
|
||||
|
||||
CardCategory: "normal",
|
||||
CostPrice: 1000,
|
||||
DistributePrice: 1500,
|
||||
Status: 1,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
|
||||
t.Run("通过ICCID查询单卡详情-成功", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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, testICCID, dataMap["iccid"])
|
||||
assert.Equal(t, "13800000001", dataMap["msisdn"])
|
||||
assert.Equal(t, "CMCC", dataMap["carrier_type"], "应返回冗余的运营商类型")
|
||||
assert.Equal(t, "测试运营商", dataMap["carrier_name"], "应返回冗余的运营商名称")
|
||||
})
|
||||
|
||||
t.Run("通过不存在的ICCID查询-应返回错误", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/iot-cards/by-iccid/NONEXISTENT_ICCID", nil)
|
||||
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, "不存在的ICCID应返回错误码")
|
||||
})
|
||||
|
||||
t.Run("未认证请求-应返回错误", func(t *testing.T) {
|
||||
url := fmt.Sprintf("/api/admin/iot-cards/by-iccid/%s", card.ICCID)
|
||||
resp, err := env.ClearAuth().Request("GET", url, nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIotCard_BatchSetSeriesBinding(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
agentAccount := env.CreateTestAccount(fmt.Sprintf("agent_%d", time.Now().UnixNano()), "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
// 创建套餐系列和分配
|
||||
series := createTestPackageSeries(t, env, "测试系列")
|
||||
createTestAllocation(t, env, shop.ID, series.ID, 0)
|
||||
|
||||
// 创建测试卡(归属于该店铺)
|
||||
timestamp := time.Now().Unix() % 1000000
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: fmt.Sprintf("TEST%06d001", timestamp), CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: fmt.Sprintf("TEST%06d002", timestamp), CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
{ICCID: fmt.Sprintf("TEST%06d003", timestamp), CarrierID: 1, Status: 1, ShopID: &shop.ID},
|
||||
}
|
||||
for _, card := range cards {
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
}
|
||||
|
||||
t.Run("批量设置卡系列绑定-成功", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID, cards[1].ICCID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, "应返回成功: %s", result.Message)
|
||||
|
||||
// 验证响应数据
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(2), dataMap["success_count"], "应有2张卡成功绑定")
|
||||
assert.Equal(t, float64(0), dataMap["fail_count"], "应无失败")
|
||||
|
||||
// 验证数据库中数据已更新
|
||||
var updatedCard model.IotCard
|
||||
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, updatedCard.SeriesID)
|
||||
assert.Equal(t, series.ID, *updatedCard.SeriesID)
|
||||
})
|
||||
|
||||
t.Run("清除卡系列绑定-series_id=0", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID},
|
||||
"series_id": 0,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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 updatedCard model.IotCard
|
||||
err = env.RawDB().Where("iccid = ?", cards[0].ICCID).First(&updatedCard).Error
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, updatedCard.SeriesID, "系列分配应被清除")
|
||||
})
|
||||
|
||||
t.Run("批量设置-部分卡不存在", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID, "NONEXISTENT_ICCID_999"},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, float64(1), dataMap["success_count"], "应有1张卡成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应有1张卡失败")
|
||||
|
||||
// 验证失败列表
|
||||
failedItems := dataMap["failed_items"].([]interface{})
|
||||
assert.Len(t, failedItems, 1)
|
||||
failedItem := failedItems[0].(map[string]interface{})
|
||||
assert.Equal(t, "NONEXISTENT_ICCID_999", failedItem["iccid"])
|
||||
})
|
||||
|
||||
t.Run("设置不存在的系列分配-应失败", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID},
|
||||
"series_id": 999999,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, "不存在的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("设置禁用的系列-应失败", func(t *testing.T) {
|
||||
// 创建一个禁用的分配
|
||||
disabledSeries := createTestPackageSeries(t, env, "禁用系列")
|
||||
env.TX.Model(&model.PackageSeries{}).Where("id = ?", disabledSeries.ID).Update("status", constants.StatusDisabled)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[2].ICCID},
|
||||
"series_id": disabledSeries.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, "禁用的系列分配应返回错误")
|
||||
})
|
||||
|
||||
t.Run("代理商设置其他店铺的卡-应失败", func(t *testing.T) {
|
||||
// 创建另一个店铺和卡
|
||||
otherShop := env.CreateTestShop("其他店铺", 1, nil)
|
||||
otherCard := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("OTH%010d", time.Now().Unix()%10000000000),
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &otherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(otherCard).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{otherCard.ICCID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证全部失败(因为卡不属于当前店铺)
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
assert.Equal(t, float64(0), dataMap["success_count"], "不应有成功")
|
||||
assert.Equal(t, float64(1), dataMap["fail_count"], "应全部失败")
|
||||
})
|
||||
|
||||
t.Run("超级管理员可以设置任意店铺的卡", func(t *testing.T) {
|
||||
// 创建另一个店铺和卡
|
||||
anotherShop := env.CreateTestShop("另一个店铺", 1, nil)
|
||||
anotherCard := &model.IotCard{
|
||||
ICCID: fmt.Sprintf("ADM%010d", time.Now().Unix()%10000000000),
|
||||
|
||||
CarrierID: 1,
|
||||
Status: 1,
|
||||
ShopID: &anotherShop.ID,
|
||||
}
|
||||
require.NoError(t, env.TX.Create(anotherCard).Error)
|
||||
|
||||
// 为这个店铺创建系列分配
|
||||
createTestAllocation(t, env, anotherShop.ID, series.ID, 0)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{anotherCard.ICCID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, float64(1), dataMap["success_count"])
|
||||
})
|
||||
|
||||
t.Run("未认证请求应返回错误", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{cards[0].ICCID},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.ClearAuth().Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
|
||||
t.Run("空ICCID列表-返回成功但无操作", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"iccids": []string{},
|
||||
"series_id": series.ID,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("PATCH", "/api/admin/iot-cards/series-binding", jsonBody)
|
||||
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["success_count"], "空列表无成功项")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err, "创建测试套餐系列失败")
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createTestAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID, allocatorShopID uint) *model.ShopPackageAllocation {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
pkg := &model.Package{
|
||||
PackageCode: fmt.Sprintf("PKG_%d", timestamp),
|
||||
PackageName: "测试套餐",
|
||||
SeriesID: seriesID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
RealDataMB: 1024,
|
||||
CostPrice: 5000,
|
||||
SuggestedRetailPrice: 12800,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := env.TX.Create(pkg).Error
|
||||
require.NoError(t, err, "创建测试套餐失败")
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: shopID,
|
||||
PackageID: pkg.ID,
|
||||
AllocatorShopID: allocatorShopID,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err = env.TX.Create(allocation).Error
|
||||
require.NoError(t, err, "创建测试分配失败")
|
||||
|
||||
return allocation
|
||||
}
|
||||
@@ -1,524 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestRequestIDMiddleware 测试 RequestID 中间件生成 UUID v4(T043)
|
||||
func TestRequestIDMiddleware(t *testing.T) {
|
||||
app := fiber.New()
|
||||
|
||||
// 配置 requestid 中间件使用 UUID v4
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
requestID := c.Locals(constants.ContextKeyRequestID)
|
||||
return c.JSON(fiber.Map{
|
||||
"request_id": requestID,
|
||||
})
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{name: "request 1"},
|
||||
{name: "request 2"},
|
||||
{name: "request 3"},
|
||||
}
|
||||
|
||||
seenIDs := make(map[string]bool)
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证响应头包含 X-Request-ID
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
if requestID == "" {
|
||||
t.Error("X-Request-ID header should not be empty")
|
||||
}
|
||||
|
||||
// 验证是 UUID v4 格式
|
||||
if _, err := uuid.Parse(requestID); err != nil {
|
||||
t.Errorf("X-Request-ID is not a valid UUID: %s, error: %v", requestID, err)
|
||||
}
|
||||
|
||||
// 验证 UUID 是唯一的
|
||||
if seenIDs[requestID] {
|
||||
t.Errorf("Request ID %s is not unique", requestID)
|
||||
}
|
||||
seenIDs[requestID] = true
|
||||
|
||||
t.Logf("Request ID: %s", requestID)
|
||||
})
|
||||
}
|
||||
|
||||
// 验证生成了多个不同的 ID
|
||||
if len(seenIDs) != len(tests) {
|
||||
t.Errorf("Expected %d unique request IDs, got %d", len(tests), len(seenIDs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoggerMiddleware 测试 Logger 中间件记录访问日志(T044)
|
||||
func TestLoggerMiddleware(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
accessLogFile := filepath.Join(tempDir, "access.log")
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: accessLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
app.Post("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendStatus(201)
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "GET request",
|
||||
method: "GET",
|
||||
path: "/test",
|
||||
expectedStatus: 200,
|
||||
},
|
||||
{
|
||||
name: "POST request",
|
||||
method: "POST",
|
||||
path: "/test",
|
||||
expectedStatus: 201,
|
||||
},
|
||||
{
|
||||
name: "GET with query params",
|
||||
method: "GET",
|
||||
path: "/test?foo=bar",
|
||||
expectedStatus: 200,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tt.method, tt.path, nil)
|
||||
req.Header.Set("User-Agent", "test-agent/1.0")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tt.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, resp.StatusCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 刷新日志缓冲区
|
||||
_ = logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 验证访问日志文件存在且有内容
|
||||
content, err := os.ReadFile(accessLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read access log: %v", err)
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
t.Error("Access log should not be empty")
|
||||
}
|
||||
|
||||
logContent := string(content)
|
||||
t.Logf("Access log content:\n%s", logContent)
|
||||
|
||||
// 验证日志包含必要的字段
|
||||
requiredFields := []string{
|
||||
"method",
|
||||
"path",
|
||||
"status",
|
||||
"duration_ms",
|
||||
"request_id",
|
||||
"ip",
|
||||
"user_agent",
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(logContent, field) {
|
||||
t.Errorf("Access log should contain field '%s'", field)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证记录了所有请求
|
||||
lines := strings.Split(strings.TrimSpace(logContent), "\n")
|
||||
if len(lines) < len(tests) {
|
||||
t.Errorf("Expected at least %d log entries, got %d", len(tests), len(lines))
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequestIDPropagation 测试 Request ID 在中间件链中传播(T045)
|
||||
func TestRequestIDPropagation(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
var capturedRequestID string
|
||||
|
||||
// 1. RequestID 中间件(第一个)
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
// 2. Logger 中间件(第二个)
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
// 3. 自定义中间件验证 request ID 是否可访问
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
requestID := c.Locals(constants.ContextKeyRequestID)
|
||||
if requestID == nil {
|
||||
t.Error("Request ID should be available in middleware chain")
|
||||
}
|
||||
if rid, ok := requestID.(string); ok {
|
||||
capturedRequestID = rid
|
||||
}
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
// 在 handler 中也验证 request ID
|
||||
requestID := c.Locals(constants.ContextKeyRequestID)
|
||||
if requestID == nil {
|
||||
return c.Status(500).SendString("Request ID not found in handler")
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"request_id": requestID,
|
||||
"message": "Request ID propagated successfully",
|
||||
})
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证响应
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证响应头中的 Request ID
|
||||
headerRequestID := resp.Header.Get("X-Request-ID")
|
||||
if headerRequestID == "" {
|
||||
t.Error("X-Request-ID header should be set")
|
||||
}
|
||||
|
||||
// 验证中间件捕获的 Request ID 与响应头一致
|
||||
if capturedRequestID != headerRequestID {
|
||||
t.Errorf("Request ID mismatch: middleware=%s, header=%s", capturedRequestID, headerRequestID)
|
||||
}
|
||||
|
||||
// 验证响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(body), headerRequestID) {
|
||||
t.Errorf("Response body should contain request ID %s", headerRequestID)
|
||||
}
|
||||
|
||||
t.Logf("Request ID successfully propagated: %s", headerRequestID)
|
||||
}
|
||||
|
||||
// TestMiddlewareOrder 测试中间件执行顺序(T045)
|
||||
func TestMiddlewareOrder(t *testing.T) {
|
||||
app := fiber.New()
|
||||
|
||||
executionOrder := []string{}
|
||||
|
||||
// 中间件 1: RequestID
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
executionOrder = append(executionOrder, "requestid-start")
|
||||
c.Locals(constants.ContextKeyRequestID, uuid.NewString())
|
||||
err := c.Next()
|
||||
executionOrder = append(executionOrder, "requestid-end")
|
||||
return err
|
||||
})
|
||||
|
||||
// 中间件 2: Logger
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
executionOrder = append(executionOrder, "logger-start")
|
||||
// 验证 Request ID 已经设置
|
||||
if c.Locals(constants.ContextKeyRequestID) == nil {
|
||||
t.Error("Request ID should be set before logger middleware")
|
||||
}
|
||||
err := c.Next()
|
||||
executionOrder = append(executionOrder, "logger-end")
|
||||
return err
|
||||
})
|
||||
|
||||
// 中间件 3: Custom
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
executionOrder = append(executionOrder, "custom-start")
|
||||
err := c.Next()
|
||||
executionOrder = append(executionOrder, "custom-end")
|
||||
return err
|
||||
})
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
executionOrder = append(executionOrder, "handler")
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 验证执行顺序
|
||||
expectedOrder := []string{
|
||||
"requestid-start",
|
||||
"logger-start",
|
||||
"custom-start",
|
||||
"handler",
|
||||
"custom-end",
|
||||
"logger-end",
|
||||
"requestid-end",
|
||||
}
|
||||
|
||||
if len(executionOrder) != len(expectedOrder) {
|
||||
t.Errorf("Expected %d execution steps, got %d", len(expectedOrder), len(executionOrder))
|
||||
}
|
||||
|
||||
for i, expected := range expectedOrder {
|
||||
if i >= len(executionOrder) {
|
||||
t.Errorf("Missing execution step at index %d: expected '%s'", i, expected)
|
||||
continue
|
||||
}
|
||||
if executionOrder[i] != expected {
|
||||
t.Errorf("Execution order mismatch at index %d: expected '%s', got '%s'", i, expected, executionOrder[i])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Middleware execution order: %v", executionOrder)
|
||||
}
|
||||
|
||||
func TestLoggerMiddlewareWithUserID(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
accessLogFile := filepath.Join(tempDir, "access-userid.log")
|
||||
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: accessLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
c.Locals(constants.ContextKeyUserID, uint(12345))
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
_ = logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
content, err := os.ReadFile(accessLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read access log: %v", err)
|
||||
}
|
||||
|
||||
logContent := string(content)
|
||||
if !strings.Contains(logContent, "12345") {
|
||||
t.Error("Access log should contain user_id '12345'")
|
||||
}
|
||||
|
||||
t.Logf("Access log with user_id:\n%s", logContent)
|
||||
}
|
||||
|
||||
// TestConcurrentRequests 测试并发请求的 Request ID 唯一性(T043)
|
||||
func TestConcurrentRequests(t *testing.T) {
|
||||
app := fiber.New()
|
||||
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
// 模拟一些处理时间
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
requestID := c.Locals(constants.ContextKeyRequestID)
|
||||
return c.JSON(fiber.Map{
|
||||
"request_id": requestID,
|
||||
})
|
||||
})
|
||||
|
||||
// 并发发送多个请求
|
||||
const numRequests = 50
|
||||
requestIDs := make(chan string, numRequests)
|
||||
errors := make(chan error, numRequests)
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go func() {
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
requestIDs <- requestID
|
||||
errors <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
// 收集所有结果
|
||||
seenIDs := make(map[string]bool)
|
||||
for i := 0; i < numRequests; i++ {
|
||||
if err := <-errors; err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
requestID := <-requestIDs
|
||||
|
||||
if requestID == "" {
|
||||
t.Error("Request ID should not be empty")
|
||||
}
|
||||
|
||||
if seenIDs[requestID] {
|
||||
t.Errorf("Duplicate request ID found: %s", requestID)
|
||||
}
|
||||
seenIDs[requestID] = true
|
||||
}
|
||||
|
||||
// 验证所有 ID 都是唯一的
|
||||
if len(seenIDs) != numRequests {
|
||||
t.Errorf("Expected %d unique request IDs, got %d", numRequests, len(seenIDs))
|
||||
}
|
||||
|
||||
t.Logf("Successfully generated %d unique request IDs concurrently", len(seenIDs))
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMigration_NoForeignKeys 验证迁移脚本不包含外键约束
|
||||
func TestMigration_NoForeignKeys(t *testing.T) {
|
||||
migrationsPath := testutils.GetMigrationsPath()
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(migrationsPath, "*.up.sql"))
|
||||
require.NoError(t, err)
|
||||
|
||||
forbiddenKeywords := []string{
|
||||
"FOREIGN KEY",
|
||||
"REFERENCES",
|
||||
"ON DELETE CASCADE",
|
||||
"ON UPDATE CASCADE",
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
content, err := os.ReadFile(file)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, keyword := range forbiddenKeywords {
|
||||
assert.NotContains(t, string(content), keyword,
|
||||
"迁移文件 %s 不应包含外键约束关键字: %s", filepath.Base(file), keyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkgGorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ==================== Part 1: 套餐系列 API 测试 ====================
|
||||
|
||||
func TestPackageSeriesAPI_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/package-series", jsonBody)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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.TX.Create(series).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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.TX.Create(s).Error)
|
||||
}
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/package-series?page=1&page_size=20", nil)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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.TX.Create(series).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"series_name": "更新后的系列名称",
|
||||
"description": "更新后的描述",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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.TX.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)
|
||||
resp, err := env.AsSuperAdmin().Request("PATCH", url, jsonBody)
|
||||
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.RawDB().First(&updatedSeries, series.ID)
|
||||
assert.Equal(t, constants.StatusDisabled, updatedSeries.Status)
|
||||
}
|
||||
|
||||
func TestPackageSeriesAPI_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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.TX.Create(series).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/package-series/%d", series.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
|
||||
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.RawDB().First(&deletedSeries, series.ID).Error
|
||||
assert.Error(t, err, "删除后应查不到套餐系列")
|
||||
}
|
||||
|
||||
// ==================== Part 2: 套餐 API 测试 ====================
|
||||
|
||||
func TestPackageAPI_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/packages", jsonBody)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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)
|
||||
|
||||
createResp, err := env.AsSuperAdmin().Request("POST", "/api/admin/packages", jsonBody)
|
||||
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)
|
||||
|
||||
shelfResp, err := env.AsSuperAdmin().Request("PATCH", fmt.Sprintf("/api/admin/packages/%d/shelf", pkgID), shelfJsonBody)
|
||||
require.NoError(t, err)
|
||||
defer shelfResp.Body.Close()
|
||||
|
||||
// 禁用套餐
|
||||
disableBody := map[string]interface{}{
|
||||
"status": constants.StatusDisabled,
|
||||
}
|
||||
disableJsonBody, _ := json.Marshal(disableBody)
|
||||
|
||||
disableResp, err := env.AsSuperAdmin().Request("PATCH", fmt.Sprintf("/api/admin/packages/%d/status", pkgID), disableJsonBody)
|
||||
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.RawDB().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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
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)
|
||||
|
||||
createResp, err := env.AsSuperAdmin().Request("POST", "/api/admin/packages", jsonBody)
|
||||
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)
|
||||
|
||||
disableResp, err := env.AsSuperAdmin().Request("PATCH", fmt.Sprintf("/api/admin/packages/%d/status", pkgID), disableJsonBody)
|
||||
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)
|
||||
|
||||
shelfResp, err := env.AsSuperAdmin().Request("PATCH", fmt.Sprintf("/api/admin/packages/%d/shelf", pkgID), shelfJsonBody)
|
||||
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.RawDB().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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.TX.Create(pkg).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
pkgList := []*model.Package{
|
||||
{
|
||||
PackageCode: fmt.Sprintf("TEST_LIST_%d_001", timestamp),
|
||||
PackageName: "列表测试套餐1",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
{
|
||||
PackageCode: fmt.Sprintf("TEST_LIST_%d_002", timestamp),
|
||||
PackageName: "列表测试套餐2",
|
||||
PackageType: "addon",
|
||||
DurationMonths: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{Creator: 1},
|
||||
},
|
||||
}
|
||||
for _, p := range pkgList {
|
||||
require.NoError(t, env.TX.Create(p).Error)
|
||||
}
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/packages?page=1&page_size=20", nil)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "原始套餐名称",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.TX.Create(pkg).Error)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"package_name": "更新后的套餐名称",
|
||||
"price": 119900,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
packageCode := fmt.Sprintf("TEST_PKG_%d", timestamp)
|
||||
|
||||
pkg := &model.Package{
|
||||
PackageCode: packageCode,
|
||||
PackageName: "测试套餐",
|
||||
PackageType: "formal",
|
||||
DurationMonths: 12,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 2,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
},
|
||||
}
|
||||
require.NoError(t, env.TX.Create(pkg).Error)
|
||||
|
||||
url := fmt.Sprintf("/api/admin/packages/%d", pkg.ID)
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", url, nil)
|
||||
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.RawDB().First(&deletedPkg, pkg.ID).Error
|
||||
assert.Error(t, err, "删除后应查不到套餐")
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// MockPermissionChecker 模拟权限检查器
|
||||
type MockPermissionChecker struct {
|
||||
permissions map[uint]map[string]bool // userID -> permCode -> hasPermission
|
||||
}
|
||||
|
||||
func NewMockPermissionChecker() *MockPermissionChecker {
|
||||
return &MockPermissionChecker{
|
||||
permissions: make(map[uint]map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockPermissionChecker) GrantPermission(userID uint, permCode string) {
|
||||
if m.permissions[userID] == nil {
|
||||
m.permissions[userID] = make(map[string]bool)
|
||||
}
|
||||
m.permissions[userID][permCode] = true
|
||||
}
|
||||
|
||||
func (m *MockPermissionChecker) CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error) {
|
||||
if m.permissions[userID] == nil {
|
||||
return false, nil
|
||||
}
|
||||
return m.permissions[userID][permCode], nil
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_RequirePermission 测试权限校验中间件(单个权限)
|
||||
func TestPermissionMiddleware_RequirePermission(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
checker.GrantPermission(1, "user:read")
|
||||
|
||||
ctx := context.Background()
|
||||
hasPermission, err := checker.CheckPermission(ctx, 1, "user:read", constants.PlatformAll)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
|
||||
hasPermission, err = checker.CheckPermission(ctx, 1, "user:write", constants.PlatformAll)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_RequireAnyPermission 测试权限校验中间件(多个权限任一)
|
||||
func TestPermissionMiddleware_RequireAnyPermission(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
checker.GrantPermission(1, "user:read")
|
||||
|
||||
ctx := context.Background()
|
||||
hasRead, _ := checker.CheckPermission(ctx, 1, "user:read", constants.PlatformAll)
|
||||
hasWrite, _ := checker.CheckPermission(ctx, 1, "user:write", constants.PlatformAll)
|
||||
|
||||
assert.True(t, hasRead || hasWrite)
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_RequireAllPermissions 测试权限校验中间件(多个权限全部)
|
||||
func TestPermissionMiddleware_RequireAllPermissions(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
checker.GrantPermission(1, "user:read")
|
||||
checker.GrantPermission(1, "user:write")
|
||||
|
||||
ctx := context.Background()
|
||||
hasRead, _ := checker.CheckPermission(ctx, 1, "user:read", constants.PlatformAll)
|
||||
hasWrite, _ := checker.CheckPermission(ctx, 1, "user:write", constants.PlatformAll)
|
||||
|
||||
assert.True(t, hasRead && hasWrite)
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_SkipSuperAdmin 测试超级管理员跳过权限检查
|
||||
func TestPermissionMiddleware_SkipSuperAdmin(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
|
||||
ctx := context.Background()
|
||||
hasPermission, err := checker.CheckPermission(ctx, 999, "any:permission", constants.PlatformAll)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_PlatformFiltering 测试按 platform 过滤权限
|
||||
func TestPermissionMiddleware_PlatformFiltering(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
checker.GrantPermission(1, "order:manage")
|
||||
|
||||
ctx := context.Background()
|
||||
hasPermissionWeb, _ := checker.CheckPermission(ctx, 1, "order:manage", constants.PlatformWeb)
|
||||
hasPermissionH5, _ := checker.CheckPermission(ctx, 1, "order:manage", constants.PlatformH5)
|
||||
|
||||
assert.True(t, hasPermissionWeb || hasPermissionH5)
|
||||
}
|
||||
|
||||
// TestPermissionMiddleware_Unauthorized 测试未认证用户访问受保护路由
|
||||
func TestPermissionMiddleware_Unauthorized(t *testing.T) {
|
||||
checker := NewMockPermissionChecker()
|
||||
|
||||
ctx := context.Background()
|
||||
hasPermission, err := checker.CheckPermission(ctx, 0, "user:read", constants.PlatformAll)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
}
|
||||
|
||||
// 集成测试实现指南:
|
||||
//
|
||||
// 完整的集成测试应该:
|
||||
// 1. 启动 Fiber 应用
|
||||
// 2. 注册受权限保护的路由:
|
||||
// - 使用 middleware.RequirePermission("user:read", config)
|
||||
// - 使用 middleware.RequireAnyPermission([]string{"user:read", "user:write"}, config)
|
||||
// - 使用 middleware.RequireAllPermissions([]string{"user:read", "user:write"}, config)
|
||||
// 3. 模拟不同用户的 HTTP 请求
|
||||
// 4. 验证权限检查结果(200 OK 或 403 Forbidden)
|
||||
//
|
||||
// 示例代码结构:
|
||||
//
|
||||
// func TestPermissionMiddleware_Integration(t *testing.T) {
|
||||
// // 1. 初始化数据库和 Redis
|
||||
// tx := testutils.NewTestTransaction(t)
|
||||
// rdb := testutils.GetTestRedis(t)
|
||||
// testutils.CleanTestRedisKeys(t, rdb)
|
||||
//
|
||||
// // 2. 创建测试数据(用户、角色、权限)
|
||||
// // ...
|
||||
//
|
||||
// // 3. 初始化 Service 和 Middleware
|
||||
// permissionService := permission.New(permissionStore)
|
||||
// config := middleware.PermissionConfig{
|
||||
// PermissionChecker: permissionService,
|
||||
// Platform: constants.PlatformWeb,
|
||||
// SkipSuperAdmin: true,
|
||||
// }
|
||||
//
|
||||
// // 4. 创建 Fiber 应用并注册路由
|
||||
// app := fiber.New()
|
||||
// app.Get("/protected",
|
||||
// middleware.RequirePermission("user:read", config),
|
||||
// func(c *fiber.Ctx) error {
|
||||
// return c.JSON(fiber.Map{"message": "success"})
|
||||
// },
|
||||
// )
|
||||
//
|
||||
// // 5. 模拟请求并验证响应
|
||||
// req := httptest.NewRequest("GET", "/protected", nil)
|
||||
// // 设置认证信息...
|
||||
// resp, err := app.Test(req)
|
||||
// require.NoError(t, err)
|
||||
// assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
// }
|
||||
@@ -1,368 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
func TestPermissionAPI_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("成功创建权限", func(t *testing.T) {
|
||||
// 权限编码必须符合 module:action 格式(两边都以小写字母开头)
|
||||
permCode := fmt.Sprintf("test:action%d", time.Now().UnixNano())
|
||||
reqBody := dto.CreatePermissionRequest{
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: permCode,
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
URL: "/admin/users",
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/permissions", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var count int64
|
||||
env.RawDB().Model(&model.Permission{}).Where("perm_code = ?", permCode).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
|
||||
t.Run("权限编码重复时返回错误", func(t *testing.T) {
|
||||
existingPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("test:dup%d", time.Now().UnixNano()),
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
reqBody := dto.CreatePermissionRequest{
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: existingPerm.PermCode,
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/permissions", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodePermCodeExists, result.Code)
|
||||
})
|
||||
|
||||
t.Run("创建子权限", func(t *testing.T) {
|
||||
parentPermCode := fmt.Sprintf("test:parent%d", time.Now().UnixNano())
|
||||
parentPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
parentPermCode,
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
childPermCode := fmt.Sprintf("test:child%d", time.Now().UnixNano())
|
||||
reqBody := dto.CreatePermissionRequest{
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: childPermCode,
|
||||
PermType: constants.PermissionTypeButton,
|
||||
ParentID: &parentPerm.ID,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/permissions", jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var child model.Permission
|
||||
err = env.RawDB().Where("perm_code = ?", childPermCode).First(&child).Error
|
||||
require.NoError(t, err, "子权限应该已创建")
|
||||
require.NotNil(t, child.ParentID, "子权限的 ParentID 应该已设置")
|
||||
assert.Equal(t, parentPerm.ID, *child.ParentID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_Get(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("test:get%d", time.Now().UnixNano()),
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
t.Run("成功获取权限详情", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/permissions/%d", testPerm.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions/99999", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodePermissionNotFound, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("test:upd%d", time.Now().UnixNano()),
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
t.Run("成功更新权限", func(t *testing.T) {
|
||||
newName := "更新后权限"
|
||||
reqBody := dto.UpdatePermissionRequest{
|
||||
PermName: &newName,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/permissions/%d", testPerm.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var updated model.Permission
|
||||
env.RawDB().First(&updated, testPerm.ID)
|
||||
assert.Equal(t, newName, updated.PermName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("成功软删除权限", func(t *testing.T) {
|
||||
testPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("test:del%d", time.Now().UnixNano()),
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/permissions/%d", testPerm.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var deleted model.Permission
|
||||
err = env.RawDB().Unscoped().First(&deleted, testPerm.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, deleted.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
env.CreateTestPermission(fmt.Sprintf("列表测试权限_%d", i), fmt.Sprintf("list:perm%d", i), constants.PermissionTypeMenu)
|
||||
}
|
||||
|
||||
t.Run("成功获取权限列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/permissions?perm_type=%d", constants.PermissionTypeMenu), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_GetTree(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
rootPerm := env.CreateTestPermission(
|
||||
fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
fmt.Sprintf("test:root%d", time.Now().UnixNano()),
|
||||
constants.PermissionTypeMenu,
|
||||
)
|
||||
|
||||
childPermCode := fmt.Sprintf("test:child%d", time.Now().UnixNano())
|
||||
childPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: childPermCode,
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
ParentID: &rootPerm.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(childPerm)
|
||||
|
||||
grandchildPermCode := fmt.Sprintf("test:grand%d", time.Now().UnixNano())
|
||||
grandchildPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: grandchildPermCode,
|
||||
PermType: constants.PermissionTypeButton,
|
||||
ParentID: &childPerm.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(grandchildPerm)
|
||||
|
||||
t.Run("成功获取权限树", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions/tree", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_GetTreeByRoleType(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:plat%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(platformPerm)
|
||||
|
||||
customerPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:cust%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(customerPerm)
|
||||
|
||||
commonPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:comm%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "1,2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(commonPerm)
|
||||
|
||||
t.Run("按角色类型过滤权限树-平台角色", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/permissions/tree?available_for_role_type=%d", constants.RoleTypePlatform), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions/tree?available_for_role_type=2", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions/tree?platform=all&available_for_role_type=1", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionAPI_FilterByAvailableForRoleTypes(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
platformPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:fplat%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(platformPerm)
|
||||
|
||||
customerPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:fcust%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(customerPerm)
|
||||
|
||||
commonPerm := &model.Permission{
|
||||
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||
PermName: fmt.Sprintf("test_permission_%d", time.Now().UnixNano()),
|
||||
PermCode: fmt.Sprintf("test:fcomm%d", time.Now().UnixNano()),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
AvailableForRoleTypes: "1,2",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(commonPerm)
|
||||
|
||||
t.Run("过滤平台角色可用权限", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/permissions?available_for_role_type=1", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/permissions/tree?available_for_role_type=%d", constants.RoleTypePlatform), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
@@ -1,337 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// setupRateLimiterTestApp creates a Fiber app with rate limiter for testing
|
||||
func setupRateLimiterTestApp(t *testing.T, max int, expiration time.Duration) *fiber.App {
|
||||
t.Helper()
|
||||
|
||||
// Initialize logger
|
||||
appLogConfig := logger.LogRotationConfig{
|
||||
Filename: "logs/app_test.log",
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
}
|
||||
accessLogConfig := logger.LogRotationConfig{
|
||||
Filename: "logs/access_test.log",
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
}
|
||||
if err := logger.InitLoggers("info", false, appLogConfig, accessLogConfig); err != nil {
|
||||
t.Fatalf("failed to initialize logger: %v", err)
|
||||
}
|
||||
|
||||
zapLogger, _ := zap.NewDevelopment()
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(zapLogger),
|
||||
})
|
||||
|
||||
// Add rate limiter middleware (nil storage = in-memory)
|
||||
app.Use(middleware.RateLimiter(max, expiration, nil))
|
||||
|
||||
// Add test route
|
||||
app.Get("/api/v1/test", func(c *fiber.Ctx) error {
|
||||
return response.Success(c, fiber.Map{
|
||||
"message": "success",
|
||||
})
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// TestRateLimiter_LimitExceeded tests that rate limiter returns 429 when limit is exceeded
|
||||
func TestRateLimiter_LimitExceeded(t *testing.T) {
|
||||
// Create app with low limit for easy testing
|
||||
max := 5
|
||||
expiration := 1 * time.Minute
|
||||
app := setupRateLimiterTestApp(t, max, expiration)
|
||||
|
||||
// Make requests up to the limit
|
||||
for i := 1; i <= max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.100") // Simulate same IP
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode, "Request %d should succeed", i)
|
||||
}
|
||||
|
||||
// The next request should be rate limited
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.100")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Should get 429 Too Many Requests
|
||||
assert.Equal(t, 429, resp.StatusCode, "Request should be rate limited")
|
||||
|
||||
// Check response body
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
t.Logf("Rate limit response: %s", string(body))
|
||||
|
||||
// Should contain error code 1008 (CodeTooManyRequests)
|
||||
assert.Contains(t, string(body), `"code":1008`, "Response should have too many requests error code")
|
||||
// Message is in Chinese: "请求过多,请稍后重试"
|
||||
assert.Contains(t, string(body), "请求过多", "Response should have rate limit message")
|
||||
}
|
||||
|
||||
// TestRateLimiter_ResetAfterExpiration tests that rate limit resets after window expiration
|
||||
func TestRateLimiter_ResetAfterExpiration(t *testing.T) {
|
||||
// Create app with short expiration for testing
|
||||
max := 3
|
||||
expiration := 2 * time.Second
|
||||
app := setupRateLimiterTestApp(t, max, expiration)
|
||||
|
||||
// Make requests up to the limit
|
||||
for i := 1; i <= max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.101")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode, "Request %d should succeed", i)
|
||||
}
|
||||
|
||||
// Next request should be rate limited
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.101")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 429, resp.StatusCode, "Request should be rate limited")
|
||||
|
||||
// Wait for rate limit window to expire
|
||||
t.Log("Waiting for rate limit window to reset...")
|
||||
time.Sleep(expiration + 500*time.Millisecond)
|
||||
|
||||
// Request should succeed after reset
|
||||
req = httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.101")
|
||||
|
||||
resp, err = app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode, "Request should succeed after rate limit reset")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Contains(t, string(body), `"code":0`, "Response should be successful after reset")
|
||||
}
|
||||
|
||||
// TestRateLimiter_PerIPRateLimiting tests that different IPs have separate rate limits
|
||||
func TestRateLimiter_PerIPRateLimiting(t *testing.T) {
|
||||
max := 5
|
||||
expiration := 1 * time.Minute
|
||||
|
||||
// Test with multiple different IPs
|
||||
ips := []string{
|
||||
"192.168.1.10",
|
||||
"192.168.1.20",
|
||||
"192.168.1.30",
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
ip := ip // Capture for closure
|
||||
t.Run(fmt.Sprintf("IP_%s", ip), func(t *testing.T) {
|
||||
// Create fresh app for each IP test to avoid shared limiter state
|
||||
freshApp := setupRateLimiterTestApp(t, max, expiration)
|
||||
|
||||
// Each IP should be able to make 'max' successful requests
|
||||
for i := 1; i <= max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", ip)
|
||||
|
||||
resp, err := freshApp.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode, "IP %s request %d should succeed", ip, i)
|
||||
}
|
||||
|
||||
// The next request for this IP should be rate limited
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", ip)
|
||||
|
||||
resp, err := freshApp.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 429, resp.StatusCode, "IP %s should be rate limited", ip)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRateLimiter_ConcurrentRequests tests rate limiter with concurrent requests from same IP
|
||||
func TestRateLimiter_ConcurrentRequests(t *testing.T) {
|
||||
// Create app with limit
|
||||
max := 10
|
||||
expiration := 1 * time.Minute
|
||||
app := setupRateLimiterTestApp(t, max, expiration)
|
||||
|
||||
// Make concurrent requests
|
||||
concurrentRequests := 15
|
||||
results := make(chan int, concurrentRequests)
|
||||
|
||||
for i := 0; i < concurrentRequests; i++ {
|
||||
go func() {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.200")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
if err != nil {
|
||||
results <- 0
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
results <- resp.StatusCode
|
||||
}()
|
||||
}
|
||||
|
||||
// Collect results
|
||||
var successCount, rateLimitedCount int
|
||||
for i := 0; i < concurrentRequests; i++ {
|
||||
status := <-results
|
||||
if status == 200 {
|
||||
successCount++
|
||||
} else if status == 429 {
|
||||
rateLimitedCount++
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Concurrent requests: %d success, %d rate limited", successCount, rateLimitedCount)
|
||||
|
||||
// Should have exactly 'max' successful requests
|
||||
assert.Equal(t, max, successCount, "Should have exactly max successful requests")
|
||||
|
||||
// Remaining requests should be rate limited
|
||||
assert.Equal(t, concurrentRequests-max, rateLimitedCount, "Remaining requests should be rate limited")
|
||||
}
|
||||
|
||||
// TestRateLimiter_DifferentLimits tests rate limiter configuration with different limits
|
||||
func TestRateLimiter_DifferentLimits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
max int
|
||||
expiration time.Duration
|
||||
}{
|
||||
{
|
||||
name: "low_limit",
|
||||
max: 2,
|
||||
expiration: 1 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "medium_limit",
|
||||
max: 10,
|
||||
expiration: 1 * time.Minute,
|
||||
},
|
||||
{
|
||||
name: "high_limit",
|
||||
max: 100,
|
||||
expiration: 1 * time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := setupRateLimiterTestApp(t, tt.max, tt.expiration)
|
||||
|
||||
// Make requests up to limit
|
||||
for i := 1; i <= tt.max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", fmt.Sprintf("192.168.1.%d", 50+i))
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Next request should be rate limited
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", fmt.Sprintf("192.168.1.%d", 50))
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 429, resp.StatusCode, "Should be rate limited after %d requests", tt.max)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRateLimiter_ShortWindow tests rate limiter with very short time window
|
||||
func TestRateLimiter_ShortWindow(t *testing.T) {
|
||||
// Create app with short window
|
||||
max := 3
|
||||
expiration := 1 * time.Second
|
||||
app := setupRateLimiterTestApp(t, max, expiration)
|
||||
|
||||
// Make first batch of requests
|
||||
for i := 1; i <= max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.250")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
}
|
||||
|
||||
// Should be rate limited now
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.250")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 429, resp.StatusCode)
|
||||
|
||||
// Wait for window to expire
|
||||
time.Sleep(expiration + 200*time.Millisecond)
|
||||
|
||||
// Should be able to make requests again
|
||||
for i := 1; i <= max; i++ {
|
||||
req := httptest.NewRequest("GET", "/api/v1/test", nil)
|
||||
req.Header.Set("X-Forwarded-For", "192.168.1.250")
|
||||
|
||||
resp, err := app.Test(req, -1)
|
||||
require.NoError(t, err)
|
||||
resp.Body.Close()
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode, "Request %d should succeed after window reset", i)
|
||||
}
|
||||
}
|
||||
@@ -1,622 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/logger"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/requestid"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TestPanicRecovery 测试 panic 恢复功能(T052)
|
||||
func TestPanicRecovery(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app-panic.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用(带自定义 ErrorHandler)
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(appLogger),
|
||||
})
|
||||
|
||||
// 注册中间件(recover 必须第一个)
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
// 创建会 panic 的 handler
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("intentional panic for testing")
|
||||
})
|
||||
|
||||
// 创建正常的 handler
|
||||
app.Get("/ok", func(c *fiber.Ctx) error {
|
||||
return c.SendString("ok")
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
shouldPanic bool
|
||||
expectedStatus int
|
||||
expectedCode int
|
||||
}{
|
||||
{
|
||||
name: "panic endpoint returns 500",
|
||||
path: "/panic",
|
||||
shouldPanic: true,
|
||||
expectedStatus: 500,
|
||||
expectedCode: errors.CodeInternalError,
|
||||
},
|
||||
{
|
||||
name: "normal endpoint works after panic",
|
||||
path: "/ok",
|
||||
shouldPanic: false,
|
||||
expectedStatus: 200,
|
||||
expectedCode: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证 HTTP 状态码
|
||||
if resp.StatusCode != tt.expectedStatus {
|
||||
t.Errorf("Expected status %d, got %d", tt.expectedStatus, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
if tt.shouldPanic {
|
||||
// panic 应该返回统一错误响应
|
||||
var response response.Response
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != tt.expectedCode {
|
||||
t.Errorf("Expected code %d, got %d", tt.expectedCode, response.Code)
|
||||
}
|
||||
|
||||
if response.Data != nil {
|
||||
t.Error("Error response data should be nil")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicLogging 测试 panic 日志记录和堆栈跟踪(T053)
|
||||
func TestPanicLogging(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
appLogFile := filepath.Join(tempDir, "app-panic-log.log")
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: appLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic-log.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
// 创建不同类型的 panic
|
||||
app.Get("/panic-string", func(c *fiber.Ctx) error {
|
||||
panic("string panic message")
|
||||
})
|
||||
|
||||
app.Get("/panic-error", func(c *fiber.Ctx) error {
|
||||
panic(fiber.NewError(500, "error panic message"))
|
||||
})
|
||||
|
||||
app.Get("/panic-struct", func(c *fiber.Ctx) error {
|
||||
panic(struct{ Message string }{"struct panic message"})
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedInLog []string
|
||||
unexpectedInLog []string
|
||||
}{
|
||||
{
|
||||
name: "string panic logs correctly",
|
||||
path: "/panic-string",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"string panic message",
|
||||
"stack",
|
||||
"request_id",
|
||||
"method",
|
||||
"path",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error panic logs correctly",
|
||||
path: "/panic-error",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"error panic message",
|
||||
"stack",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "struct panic logs correctly",
|
||||
path: "/panic-struct",
|
||||
expectedInLog: []string{
|
||||
"Panic 已恢复",
|
||||
"stack",
|
||||
"Message",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 执行会 panic 的请求
|
||||
req := httptest.NewRequest("GET", tt.path, nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 刷新日志缓冲区
|
||||
logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 读取日志内容
|
||||
logContent, err := os.ReadFile(appLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read app log: %v", err)
|
||||
}
|
||||
|
||||
content := string(logContent)
|
||||
|
||||
// 验证日志包含预期内容
|
||||
for _, expected := range tt.expectedInLog {
|
||||
if !strings.Contains(content, expected) {
|
||||
t.Errorf("Log should contain '%s'", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证日志不包含意外内容
|
||||
for _, unexpected := range tt.unexpectedInLog {
|
||||
if strings.Contains(content, unexpected) {
|
||||
t.Errorf("Log should NOT contain '%s'", unexpected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证堆栈跟踪包含文件和行号
|
||||
if !strings.Contains(content, "recover_test.go") {
|
||||
t.Error("Stack trace should contain source file name")
|
||||
}
|
||||
|
||||
t.Logf("Panic log contains stack trace: %v", strings.Contains(content, "stack"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubsequentRequestsAfterPanic 测试 panic 后后续请求正常处理(T054)
|
||||
func TestSubsequentRequestsAfterPanic(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
callCount := 0
|
||||
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
callCount++
|
||||
// 第 1、3、5 次调用会 panic
|
||||
if callCount%2 == 1 {
|
||||
panic("test panic")
|
||||
}
|
||||
// 第 2、4、6 次调用正常返回
|
||||
return c.JSON(fiber.Map{
|
||||
"call_count": callCount,
|
||||
"status": "ok",
|
||||
})
|
||||
})
|
||||
|
||||
// 执行多次请求,验证 panic 不影响后续请求
|
||||
for i := 1; i <= 6; i++ {
|
||||
req := httptest.NewRequest("GET", "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Request %d failed: %v", i, err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if i%2 == 1 {
|
||||
// 奇数次应该返回 500
|
||||
if resp.StatusCode != 500 {
|
||||
t.Errorf("Request %d: expected status 500, got %d", i, resp.StatusCode)
|
||||
}
|
||||
} else {
|
||||
// 偶数次应该返回 200
|
||||
if resp.StatusCode != 200 {
|
||||
t.Errorf("Request %d: expected status 200, got %d", i, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证响应内容
|
||||
var response map[string]any
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Request %d: failed to unmarshal response: %v", i, err)
|
||||
}
|
||||
|
||||
if status, ok := response["status"].(string); !ok || status != "ok" {
|
||||
t.Errorf("Request %d: expected status 'ok', got %v", i, response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Request %d completed: status=%d", i, resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证所有 6 次调用都执行了
|
||||
if callCount != 6 {
|
||||
t.Errorf("Expected 6 calls, got %d", callCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPanicWithRequestID 测试 panic 日志包含 Request ID(T053)
|
||||
func TestPanicWithRequestID(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
appLogFile := filepath.Join(tempDir, "app-panic-reqid.log")
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: appLogFile,
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access-panic-reqid.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件(顺序重要)
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("test panic with request id")
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// 获取 Request ID
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
if requestID == "" {
|
||||
t.Error("X-Request-ID header should be set even after panic")
|
||||
}
|
||||
|
||||
// 刷新日志缓冲区
|
||||
logger.Sync()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// 读取日志内容
|
||||
logContent, err := os.ReadFile(appLogFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read app log: %v", err)
|
||||
}
|
||||
|
||||
content := string(logContent)
|
||||
|
||||
// 验证日志包含 Request ID
|
||||
if !strings.Contains(content, requestID) {
|
||||
t.Errorf("Panic log should contain request ID '%s'", requestID)
|
||||
}
|
||||
|
||||
// 验证日志包含关键字段
|
||||
requiredFields := []string{
|
||||
"request_id",
|
||||
"method",
|
||||
"path",
|
||||
"panic",
|
||||
"stack",
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(content, field) {
|
||||
t.Errorf("Panic log should contain field '%s'", field)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Panic log successfully includes Request ID: %s", requestID)
|
||||
}
|
||||
|
||||
// TestConcurrentPanics 测试并发 panic 处理(T054)
|
||||
func TestConcurrentPanics(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New()
|
||||
|
||||
// 注册中间件
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("concurrent panic test")
|
||||
})
|
||||
|
||||
// 并发发送多个会 panic 的请求
|
||||
const numRequests = 20
|
||||
errors := make(chan error, numRequests)
|
||||
statuses := make(chan int, numRequests)
|
||||
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go func() {
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
statuses <- 0
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
statuses <- resp.StatusCode
|
||||
errors <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
// 收集所有结果
|
||||
for i := 0; i < numRequests; i++ {
|
||||
if err := <-errors; err != nil {
|
||||
t.Fatalf("Request failed: %v", err)
|
||||
}
|
||||
status := <-statuses
|
||||
if status != 500 {
|
||||
t.Errorf("Expected status 500, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Successfully handled %d concurrent panics", numRequests)
|
||||
}
|
||||
|
||||
// TestRecoverMiddlewareOrder 测试 Recover 中间件必须在第一个(T052)
|
||||
func TestRecoverMiddlewareOrder(t *testing.T) {
|
||||
// 创建临时目录用于日志
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 初始化日志系统
|
||||
err := logger.InitLoggers("info", false,
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
logger.LogRotationConfig{
|
||||
Filename: filepath.Join(tempDir, "access.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 7,
|
||||
Compress: false,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize loggers: %v", err)
|
||||
}
|
||||
defer func() { _ = logger.Sync() }()
|
||||
|
||||
appLogger := logger.GetAppLogger()
|
||||
|
||||
// 创建应用
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(appLogger),
|
||||
})
|
||||
|
||||
// 正确的顺序:Recover → RequestID → Logger
|
||||
app.Use(middleware.Recover(appLogger))
|
||||
app.Use(requestid.New(requestid.Config{
|
||||
Generator: func() string {
|
||||
return uuid.NewString()
|
||||
},
|
||||
}))
|
||||
app.Use(logger.Middleware())
|
||||
|
||||
app.Get("/panic", func(c *fiber.Ctx) error {
|
||||
panic("test panic")
|
||||
})
|
||||
|
||||
// 执行请求
|
||||
req := httptest.NewRequest("GET", "/panic", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to execute request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 验证请求被正确处理(返回 500 而不是崩溃)
|
||||
if resp.StatusCode != 500 {
|
||||
t.Errorf("Expected status 500, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 验证仍然有 Request ID(说明 RequestID 中间件在 Recover 之后执行)
|
||||
requestID := resp.Header.Get("X-Request-ID")
|
||||
if requestID == "" {
|
||||
t.Error("X-Request-ID should be set even after panic")
|
||||
}
|
||||
|
||||
// 解析响应,验证返回了统一错误格式
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var response response.Response
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != errors.CodeInternalError {
|
||||
t.Errorf("Expected code %d, got %d", errors.CodeInternalError, response.Code)
|
||||
}
|
||||
|
||||
t.Logf("Recover middleware correctly placed first, handled panic gracefully")
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
roleService "github.com/break/junhong_cmp_fiber/internal/service/role"
|
||||
postgresStore "github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
// TestRolePermissionAssociation_AssignPermissions 测试角色权限分配功能
|
||||
func TestRolePermissionAssociation_AssignPermissions(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
env.TX.AutoMigrate(
|
||||
&model.Role{},
|
||||
&model.Permission{},
|
||||
&model.RolePermission{},
|
||||
)
|
||||
|
||||
roleStore := postgresStore.NewRoleStore(env.TX)
|
||||
permStore := postgresStore.NewPermissionStore(env.TX)
|
||||
rolePermStore := postgresStore.NewRolePermissionStore(env.TX, env.Redis)
|
||||
roleSvc := roleService.New(roleStore, permStore, rolePermStore)
|
||||
|
||||
// 创建测试用户上下文
|
||||
userCtx := env.GetSuperAdminContext()
|
||||
|
||||
t.Run("成功分配单个权限", func(t *testing.T) {
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "单权限测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 创建测试权限
|
||||
perm := &model.Permission{
|
||||
PermName: "单权限测试",
|
||||
PermCode: "single:perm:test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
|
||||
// 分配权限
|
||||
rps, err := roleSvc.AssignPermissions(userCtx, role.ID, []uint{perm.ID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, rps, 1)
|
||||
assert.Equal(t, role.ID, rps[0].RoleID)
|
||||
assert.Equal(t, perm.ID, rps[0].PermID)
|
||||
})
|
||||
|
||||
t.Run("成功分配多个权限", func(t *testing.T) {
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "多权限测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 创建多个测试权限
|
||||
permIDs := make([]uint, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
perm := &model.Permission{
|
||||
PermName: "多权限测试_" + string(rune('A'+i)),
|
||||
PermCode: "multi:perm:test:" + string(rune('a'+i)),
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
permIDs[i] = perm.ID
|
||||
}
|
||||
|
||||
// 分配权限
|
||||
rps, err := roleSvc.AssignPermissions(userCtx, role.ID, permIDs)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, rps, 3)
|
||||
})
|
||||
|
||||
t.Run("获取角色的权限列表", func(t *testing.T) {
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "获取权限列表测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 创建并分配权限
|
||||
perm := &model.Permission{
|
||||
PermName: "获取权限列表测试",
|
||||
PermCode: "get:perm:list:test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
|
||||
_, err := roleSvc.AssignPermissions(userCtx, role.ID, []uint{perm.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 获取权限列表
|
||||
perms, err := roleSvc.GetPermissions(userCtx, role.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, perms, 1)
|
||||
assert.Equal(t, perm.ID, perms[0].ID)
|
||||
})
|
||||
|
||||
t.Run("移除角色的权限", func(t *testing.T) {
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "移除权限测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 创建并分配权限
|
||||
perm := &model.Permission{
|
||||
PermName: "移除权限测试",
|
||||
PermCode: "remove:perm:test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
|
||||
_, err := roleSvc.AssignPermissions(userCtx, role.ID, []uint{perm.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 移除权限
|
||||
err = roleSvc.RemovePermission(userCtx, role.ID, perm.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证权限已被软删除
|
||||
var rp model.RolePermission
|
||||
err = env.RawDB().Unscoped().Where("role_id = ? AND perm_id = ?", role.ID, perm.ID).First(&rp).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, rp.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("重复分配权限不会创建重复记录", func(t *testing.T) {
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "重复权限测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(role)
|
||||
|
||||
// 创建测试权限
|
||||
perm := &model.Permission{
|
||||
PermName: "重复权限测试",
|
||||
PermCode: "duplicate:perm:test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(perm)
|
||||
|
||||
// 第一次分配
|
||||
_, err := roleSvc.AssignPermissions(userCtx, role.ID, []uint{perm.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 第二次分配相同权限
|
||||
_, err = roleSvc.AssignPermissions(userCtx, role.ID, []uint{perm.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证只有一条记录
|
||||
var count int64
|
||||
env.RawDB().Model(&model.RolePermission{}).Where("role_id = ?", role.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count, "关联记录应该仍然存在,因为没有外键约束")
|
||||
|
||||
// 验证可以独立查询关联记录
|
||||
var rpRecord model.RolePermission
|
||||
err = env.RawDB().Where("role_id = ? AND perm_id = ?", role.ID, perm.ID).First(&rpRecord).Error
|
||||
assert.NoError(t, err, "应该能查询到关联记录")
|
||||
})
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"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/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
)
|
||||
|
||||
func TestRoleAPI_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("成功创建角色", func(t *testing.T) {
|
||||
roleName := fmt.Sprintf("test_role_%d", time.Now().UnixNano())
|
||||
reqBody := dto.CreateRoleRequest{
|
||||
RoleName: roleName,
|
||||
RoleDesc: "这是一个测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/roles", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("响应状态码: %d, 业务码: %d, 消息: %s", resp.StatusCode, result.Code, result.Message)
|
||||
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
assert.Equal(t, 0, result.Code, "业务码应为0,实际消息: %s", result.Message)
|
||||
|
||||
var count int64
|
||||
env.RawDB().Model(&model.Role{}).Where("role_name = ?", roleName).Count(&count)
|
||||
t.Logf("查询角色 '%s' 数量: %d", roleName, count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
|
||||
t.Run("缺少必填字段返回错误", func(t *testing.T) {
|
||||
reqBody := map[string]interface{}{
|
||||
"role_desc": "缺少名称",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/roles", jsonBody)
|
||||
require.NoError(t, err)
|
||||
var result response.Response
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
assert.NotEqual(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_Get(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("获取测试角色", constants.RoleTypePlatform)
|
||||
|
||||
t.Run("成功获取角色详情", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/roles/%d", testRole.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, 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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles/99999", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodeRoleNotFound, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("更新测试角色", constants.RoleTypePlatform)
|
||||
|
||||
t.Run("成功更新角色", func(t *testing.T) {
|
||||
newName := "更新后角色"
|
||||
reqBody := dto.UpdateRoleRequest{
|
||||
RoleName: &newName,
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/roles/%d", testRole.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var updated model.Role
|
||||
env.RawDB().First(&updated, testRole.ID)
|
||||
assert.Equal(t, newName, updated.RoleName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_Delete(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
t.Run("成功软删除角色", func(t *testing.T) {
|
||||
testRole := env.CreateTestRole("删除测试角色", constants.RoleTypePlatform)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/roles/%d", testRole.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var deleted model.Role
|
||||
err = env.RawDB().Unscoped().First(&deleted, testRole.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, deleted.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_List(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
env.CreateTestRole(fmt.Sprintf("test_role_%d_%d", time.Now().UnixNano(), i), constants.RoleTypePlatform)
|
||||
}
|
||||
|
||||
t.Run("成功获取角色列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles?page=1&page_size=10", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_AssignPermissions(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("权限分配测试角色", constants.RoleTypePlatform)
|
||||
testPerm := env.CreateTestPermission("测试权限", "test:permission", constants.PermissionTypeMenu)
|
||||
|
||||
t.Run("成功分配权限", func(t *testing.T) {
|
||||
reqBody := dto.AssignPermissionsRequest{
|
||||
PermIDs: []uint{testPerm.ID},
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("POST", fmt.Sprintf("/api/admin/roles/%d/permissions", testRole.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var count int64
|
||||
env.RawDB().Model(&model.RolePermission{}).Where("role_id = ? AND perm_id = ?", testRole.ID, testPerm.ID).Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_GetPermissions(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("获取权限测试角色", constants.RoleTypePlatform)
|
||||
testPerm := env.CreateTestPermission("获取权限测试", "get:permission:test", constants.PermissionTypeMenu)
|
||||
|
||||
rolePerm := &model.RolePermission{
|
||||
RoleID: testRole.ID,
|
||||
PermID: testPerm.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(rolePerm)
|
||||
|
||||
t.Run("成功获取角色权限", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", fmt.Sprintf("/api/admin/roles/%d/permissions", testRole.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_RemovePermission(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("移除权限测试角色", constants.RoleTypePlatform)
|
||||
testPerm := env.CreateTestPermission("移除权限测试", "remove:permission:test", constants.PermissionTypeMenu)
|
||||
|
||||
rolePerm := &model.RolePermission{
|
||||
RoleID: testRole.ID,
|
||||
PermID: testPerm.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
env.TX.Create(rolePerm)
|
||||
|
||||
t.Run("成功移除权限", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/roles/%d/permissions/%d", testRole.ID, testPerm.ID), nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var rp model.RolePermission
|
||||
err = env.RawDB().Unscoped().Where("role_id = ? AND perm_id = ?", testRole.ID, testPerm.ID).First(&rp).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, rp.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleAPI_UpdateStatus(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
testRole := env.CreateTestRole("状态切换测试角色", constants.RoleTypePlatform)
|
||||
|
||||
t.Run("成功禁用角色", func(t *testing.T) {
|
||||
reqBody := dto.UpdateRoleStatusRequest{
|
||||
Status: intPtr(constants.StatusDisabled),
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/roles/%d/status", testRole.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var updated model.Role
|
||||
env.RawDB().First(&updated, testRole.ID)
|
||||
assert.Equal(t, constants.StatusDisabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("成功启用角色", func(t *testing.T) {
|
||||
reqBody := dto.UpdateRoleStatusRequest{
|
||||
Status: intPtr(constants.StatusEnabled),
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/roles/%d/status", testRole.ID), jsonBody)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fiber.StatusOK, resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
|
||||
var updated model.Role
|
||||
env.RawDB().First(&updated, testRole.ID)
|
||||
assert.Equal(t, constants.StatusEnabled, updated.Status)
|
||||
})
|
||||
|
||||
t.Run("角色不存在返回错误", func(t *testing.T) {
|
||||
reqBody := dto.UpdateRoleStatusRequest{
|
||||
Status: intPtr(constants.StatusEnabled),
|
||||
}
|
||||
|
||||
jsonBody, _ := json.Marshal(reqBody)
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", "/api/admin/roles/99999/status", jsonBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, errors.CodeRoleNotFound, result.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// intPtr 返回 int 的指针
|
||||
func intPtr(v int) *int {
|
||||
return &v
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestShopManagement_CreateShop 测试创建商户
|
||||
func TestShopManagement_CreateShop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 使用时间戳生成唯一的店铺名和代码
|
||||
timestamp := time.Now().UnixNano()
|
||||
shopName := fmt.Sprintf("test_shop_%d", timestamp)
|
||||
shopCode := fmt.Sprintf("SHOP%d", timestamp%1000000)
|
||||
|
||||
reqBody := dto.CreateShopRequest{
|
||||
ShopName: shopName,
|
||||
ShopCode: shopCode,
|
||||
InitUsername: "testuser",
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shops", body)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
t.Logf("HTTP 状态码: %d", resp.StatusCode)
|
||||
|
||||
var result response.Response
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Logf("响应 Code: %d, Message: %s", result.Code, result.Message)
|
||||
t.Logf("响应 Data: %+v", result.Data)
|
||||
|
||||
if result.Code != 0 {
|
||||
t.Fatalf("API 返回错误: %s", result.Message)
|
||||
}
|
||||
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
assert.Equal(t, 0, result.Code)
|
||||
assert.NotNil(t, result.Data)
|
||||
|
||||
shopData, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, shopName, shopData["shop_name"])
|
||||
assert.Equal(t, shopCode, shopData["shop_code"])
|
||||
assert.Equal(t, float64(1), shopData["level"])
|
||||
assert.Equal(t, float64(1), shopData["status"])
|
||||
}
|
||||
|
||||
// TestShopManagement_CreateShop_DuplicateCode 测试创建商户 - 商户编码重复
|
||||
func TestShopManagement_CreateShop_DuplicateCode(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 使用时间戳生成唯一的店铺代码
|
||||
timestamp := time.Now().UnixNano()
|
||||
duplicateCode := fmt.Sprintf("DUP%d", timestamp%1000000)
|
||||
|
||||
firstReq := dto.CreateShopRequest{
|
||||
ShopName: fmt.Sprintf("shop1_%d", timestamp),
|
||||
ShopCode: duplicateCode,
|
||||
InitUsername: fmt.Sprintf("dupuser1_%d", timestamp),
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
firstBody, _ := json.Marshal(firstReq)
|
||||
firstResp, _ := env.AsSuperAdmin().Request("POST", "/api/admin/shops", firstBody)
|
||||
var firstResult response.Response
|
||||
json.NewDecoder(firstResp.Body).Decode(&firstResult)
|
||||
firstResp.Body.Close()
|
||||
|
||||
require.Equal(t, 0, firstResult.Code, "第一个商户应该创建成功")
|
||||
|
||||
reqBody := dto.CreateShopRequest{
|
||||
ShopName: fmt.Sprintf("shop2_%d", timestamp),
|
||||
ShopCode: duplicateCode,
|
||||
InitUsername: fmt.Sprintf("dupuser2_%d", timestamp),
|
||||
InitPhone: testutils.GenerateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shops", body)
|
||||
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) // 非成功状态
|
||||
assert.Contains(t, result.Message, "已存在") // 错误消息应包含"已存在"
|
||||
}
|
||||
|
||||
// TestShopManagement_ListShops 测试查询商户列表
|
||||
func TestShopManagement_ListShops(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
env.CreateTestShop("商户A", 1, nil)
|
||||
env.CreateTestShop("商户B", 1, nil)
|
||||
env.CreateTestShop("商户C", 2, nil)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/shops?page=1&size=10", nil)
|
||||
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)
|
||||
|
||||
items, ok := dataMap["items"].([]interface{})
|
||||
require.True(t, ok)
|
||||
assert.GreaterOrEqual(t, len(items), 3)
|
||||
}
|
||||
|
||||
// TestShopManagement_UpdateShop 测试更新商户
|
||||
func TestShopManagement_UpdateShop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试商户
|
||||
shop := env.CreateTestShop("原始商户", 1, nil)
|
||||
|
||||
// 更新商户
|
||||
reqBody := dto.UpdateShopRequest{
|
||||
ShopName: "更新后的商户",
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("PUT", fmt.Sprintf("/api/admin/shops/%d", shop.ID), body)
|
||||
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)
|
||||
assert.NotNil(t, result.Data)
|
||||
|
||||
shopData, ok := result.Data.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "更新后的商户", shopData["shop_name"])
|
||||
}
|
||||
|
||||
// TestShopManagement_DeleteShop 测试删除商户
|
||||
func TestShopManagement_DeleteShop(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试商户
|
||||
shop := env.CreateTestShop("待删除商户", 1, nil)
|
||||
|
||||
// 删除商户
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/shops/%d", shop.ID), nil)
|
||||
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)
|
||||
}
|
||||
|
||||
// TestShopManagement_DeleteShop_WithMultipleAccounts 测试删除商户 - 多个关联账号
|
||||
func TestShopManagement_DeleteShop_WithMultipleAccounts(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试商户
|
||||
shop := env.CreateTestShop("多账号商户", 1, nil)
|
||||
|
||||
// 删除商户
|
||||
resp, err := env.AsSuperAdmin().Request("DELETE", fmt.Sprintf("/api/admin/shops/%d", shop.ID), nil)
|
||||
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)
|
||||
}
|
||||
|
||||
// TestShopManagement_Unauthorized 测试未认证访问
|
||||
func TestShopManagement_Unauthorized(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 不提供 token
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/shops", nil)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 应该返回 401 未授权
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
}
|
||||
|
||||
// TestShopManagement_InvalidToken 测试无效 token
|
||||
func TestShopManagement_InvalidToken(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 提供无效 token
|
||||
resp, err := env.RequestWithHeaders("GET", "/api/admin/shops", nil, map[string]string{
|
||||
"Authorization": "Bearer invalid-token-12345",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 应该返回 401 未授权
|
||||
assert.Equal(t, 401, resp.StatusCode)
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBatchAllocationAPI_Create(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
parentShop := env.CreateTestShop("父级店铺", 1, nil)
|
||||
childShop := env.CreateTestShop("子级店铺", 2, &parentShop.ID)
|
||||
series := createBatchTestPackageSeries(t, env, "批量分配测试系列")
|
||||
|
||||
createBatchTestPackages(t, env, series.ID, 3)
|
||||
|
||||
t.Run("批量分配套餐_固定金额返佣", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": childShop.ID,
|
||||
"series_id": series.ID,
|
||||
"base_commission": map[string]interface{}{
|
||||
"mode": "fixed",
|
||||
"value": 1000,
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-allocations", jsonBody)
|
||||
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, "应返回成功: %s", result.Message)
|
||||
})
|
||||
|
||||
t.Run("批量分配套餐_百分比返佣", func(t *testing.T) {
|
||||
series2 := createBatchTestPackageSeries(t, env, "系列2")
|
||||
createBatchTestPackages(t, env, series2.ID, 2)
|
||||
shop2 := env.CreateTestShop("测试店铺2", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop2.ID,
|
||||
"series_id": series2.ID,
|
||||
"base_commission": map[string]interface{}{
|
||||
"mode": "percent",
|
||||
"value": 200,
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-allocations", jsonBody)
|
||||
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) {
|
||||
series3 := createBatchTestPackageSeries(t, env, "系列3")
|
||||
createBatchTestPackages(t, env, series3.ID, 2)
|
||||
shop3 := env.CreateTestShop("测试店铺3", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop3.ID,
|
||||
"series_id": series3.ID,
|
||||
"price_adjustment": map[string]interface{}{
|
||||
"type": "fixed",
|
||||
"value": 500,
|
||||
},
|
||||
"base_commission": map[string]interface{}{
|
||||
"mode": "fixed",
|
||||
"value": 800,
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-allocations", jsonBody)
|
||||
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) {
|
||||
series4 := createBatchTestPackageSeries(t, env, "系列4")
|
||||
createBatchTestPackages(t, env, series4.ID, 2)
|
||||
shop4 := env.CreateTestShop("测试店铺4", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop4.ID,
|
||||
"series_id": series4.ID,
|
||||
"base_commission": map[string]interface{}{
|
||||
"mode": "percent",
|
||||
"value": 150,
|
||||
},
|
||||
|
||||
"tier_config": map[string]interface{}{
|
||||
"period_type": "monthly",
|
||||
"tier_type": "sales_count",
|
||||
"tiers": []map[string]interface{}{
|
||||
{"threshold": 100, "mode": "percent", "value": 200},
|
||||
{"threshold": 200, "mode": "percent", "value": 250},
|
||||
{"threshold": 500, "mode": "percent", "value": 300},
|
||||
},
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-allocations", jsonBody)
|
||||
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, "启用梯度返佣应成功: %s", result.Message)
|
||||
})
|
||||
|
||||
t.Run("批量分配_系列无套餐应失败", func(t *testing.T) {
|
||||
emptySeries := createBatchTestPackageSeries(t, env, "空系列")
|
||||
shop5 := env.CreateTestShop("测试店铺5", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop5.ID,
|
||||
"series_id": emptySeries.ID,
|
||||
"base_commission": map[string]interface{}{
|
||||
"mode": "fixed",
|
||||
"value": 1000,
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-allocations", jsonBody)
|
||||
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, "空系列应返回错误")
|
||||
})
|
||||
}
|
||||
|
||||
func createBatchTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("BATCH_SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err, "创建测试套餐系列失败")
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createBatchTestPackages(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, count int) []*model.Package {
|
||||
t.Helper()
|
||||
|
||||
packages := make([]*model.Package, 0, count)
|
||||
timestamp := time.Now().UnixNano()
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
pkg := &model.Package{
|
||||
PackageCode: fmt.Sprintf("BATCH_PKG_%d_%d", timestamp, i),
|
||||
PackageName: fmt.Sprintf("批量测试套餐%d", i+1),
|
||||
SeriesID: seriesID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
CostPrice: 5000 + int64(i*500),
|
||||
SuggestedRetailPrice: 9900 + int64(i*1000),
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(pkg).Error
|
||||
require.NoError(t, err, "创建测试套餐失败")
|
||||
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBatchPricingAPI_Update(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
series := createPricingTestPackageSeries(t, env, "调价测试系列")
|
||||
packages := createPricingTestPackages(t, env, series.ID, 3)
|
||||
|
||||
for _, pkg := range packages {
|
||||
createPricingTestAllocation(t, env, shop.ID, pkg.ID, series.ID, 5000)
|
||||
}
|
||||
|
||||
t.Run("批量调价_固定金额调整", func(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop.ID,
|
||||
"series_id": series.ID,
|
||||
"price_adjustment": map[string]interface{}{
|
||||
"type": "fixed",
|
||||
"value": 1000,
|
||||
},
|
||||
"change_reason": "统一调价测试",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-pricing", jsonBody)
|
||||
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, "应返回成功: %s", result.Message)
|
||||
|
||||
if result.Data != nil {
|
||||
dataMap := result.Data.(map[string]interface{})
|
||||
updatedCount := int(dataMap["updated_count"].(float64))
|
||||
assert.Equal(t, 3, updatedCount, "应更新3个套餐分配")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("批量调价_百分比调整", func(t *testing.T) {
|
||||
shop2 := env.CreateTestShop("测试店铺2", 1, nil)
|
||||
series2 := createPricingTestPackageSeries(t, env, "系列2")
|
||||
packages2 := createPricingTestPackages(t, env, series2.ID, 2)
|
||||
|
||||
for _, pkg := range packages2 {
|
||||
createPricingTestAllocation(t, env, shop2.ID, pkg.ID, series2.ID, 10000)
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop2.ID,
|
||||
"series_id": series2.ID,
|
||||
"price_adjustment": map[string]interface{}{
|
||||
"type": "percent",
|
||||
"value": 100,
|
||||
},
|
||||
"change_reason": "加价10%",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-pricing", jsonBody)
|
||||
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{})
|
||||
updatedCount := int(dataMap["updated_count"].(float64))
|
||||
assert.Equal(t, 2, updatedCount, "应更新2个套餐分配")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("批量调价_不指定系列调整所有", func(t *testing.T) {
|
||||
shop3 := env.CreateTestShop("测试店铺3", 1, nil)
|
||||
series3a := createPricingTestPackageSeries(t, env, "系列3A")
|
||||
series3b := createPricingTestPackageSeries(t, env, "系列3B")
|
||||
|
||||
pkg3a := createPricingTestPackages(t, env, series3a.ID, 1)[0]
|
||||
pkg3b := createPricingTestPackages(t, env, series3b.ID, 1)[0]
|
||||
|
||||
createPricingTestAllocation(t, env, shop3.ID, pkg3a.ID, series3a.ID, 8000)
|
||||
createPricingTestAllocation(t, env, shop3.ID, pkg3b.ID, series3b.ID, 8000)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop3.ID,
|
||||
"price_adjustment": map[string]interface{}{
|
||||
"type": "fixed",
|
||||
"value": 500,
|
||||
},
|
||||
"change_reason": "全局调价",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-pricing", jsonBody)
|
||||
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{})
|
||||
updatedCount := int(dataMap["updated_count"].(float64))
|
||||
assert.GreaterOrEqual(t, updatedCount, 2, "应更新至少2个套餐分配")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("批量调价_无匹配记录应失败", func(t *testing.T) {
|
||||
shop4 := env.CreateTestShop("空店铺", 1, nil)
|
||||
|
||||
body := map[string]interface{}{
|
||||
"shop_id": shop4.ID,
|
||||
"price_adjustment": map[string]interface{}{
|
||||
"type": "fixed",
|
||||
"value": 1000,
|
||||
},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-package-batch-pricing", jsonBody)
|
||||
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, "无匹配记录应返回错误")
|
||||
})
|
||||
}
|
||||
|
||||
func createPricingTestPackageSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries {
|
||||
t.Helper()
|
||||
|
||||
timestamp := time.Now().UnixNano()
|
||||
series := &model.PackageSeries{
|
||||
SeriesCode: fmt.Sprintf("PRICING_SERIES_%d", timestamp),
|
||||
SeriesName: name,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(series).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func createPricingTestPackages(t *testing.T, env *integ.IntegrationTestEnv, seriesID uint, count int) []*model.Package {
|
||||
t.Helper()
|
||||
|
||||
packages := make([]*model.Package, 0, count)
|
||||
timestamp := time.Now().UnixNano()
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
pkg := &model.Package{
|
||||
PackageCode: fmt.Sprintf("PRICING_PKG_%d_%d", timestamp, i),
|
||||
PackageName: fmt.Sprintf("调价测试套餐%d", i+1),
|
||||
SeriesID: seriesID,
|
||||
PackageType: "formal",
|
||||
DurationMonths: 1,
|
||||
CostPrice: 5000,
|
||||
Status: constants.StatusEnabled,
|
||||
ShelfStatus: 1,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(pkg).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
packages = append(packages, pkg)
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
func createPricingTestAllocation(t *testing.T, env *integ.IntegrationTestEnv, shopID, packageID, seriesID uint, costPrice int64) *model.ShopPackageAllocation {
|
||||
t.Helper()
|
||||
|
||||
allocation := &model.ShopPackageAllocation{
|
||||
ShopID: shopID,
|
||||
PackageID: packageID,
|
||||
AllocatorShopID: 0,
|
||||
CostPrice: costPrice,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
|
||||
err := env.TX.Create(allocation).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return allocation
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/response"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils/integ"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStandaloneCardAllocation_AllocateByList(t *testing.T) {
|
||||
env := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
subShop := env.CreateTestShop("测试下级店铺", 2, &shop.ID)
|
||||
agentAccount := env.CreateTestAccount("agent_alloc", "password123", constants.UserTypeAgent, &shop.ID, nil)
|
||||
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "ALLOC_TEST001", CarrierID: 1, Status: constants.IotCardStatusInStock},
|
||||
{ICCID: "ALLOC_TEST002", CarrierID: 1, Status: constants.IotCardStatusInStock},
|
||||
{ICCID: "ALLOC_TEST003", CarrierID: 1, Status: constants.IotCardStatusInStock},
|
||||
}
|
||||
for _, card := range cards {
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
}
|
||||
|
||||
t.Run("平台分配卡给一级店铺", func(t *testing.T) {
|
||||
reqBody := map[string]interface{}{
|
||||
"to_shop_id": shop.ID,
|
||||
"selection_type": "list",
|
||||
"iccids": []string{"ALLOC_TEST001", "ALLOC_TEST002"},
|
||||
"remark": "测试分配",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/iot-cards/standalone/allocate", bodyBytes)
|
||||
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.RawDB().WithContext(ctx).Where("iccid IN ?", []string{"ALLOC_TEST001", "ALLOC_TEST002"}).Find(&updatedCards)
|
||||
for _, card := range updatedCards {
|
||||
assert.Equal(t, shop.ID, *card.ShopID, "卡应分配给目标店铺")
|
||||
assert.Equal(t, constants.IotCardStatusDistributed, card.Status, "状态应为已分销")
|
||||
}
|
||||
|
||||
var recordCount int64
|
||||
env.RawDB().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": subShop.ID,
|
||||
"selection_type": "list",
|
||||
"iccids": []string{"ALLOC_TEST001"},
|
||||
"remark": "代理分配测试",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsUser(agentAccount).Request("POST", "/api/admin/iot-cards/standalone/allocate", bodyBytes)
|
||||
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": shop.ID,
|
||||
"selection_type": "list",
|
||||
"iccids": []string{"NOT_EXISTS_001", "NOT_EXISTS_002"},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/iot-cards/standalone/allocate", bodyBytes)
|
||||
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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
shopID := shop.ID
|
||||
cards := []*model.IotCard{
|
||||
{ICCID: "ALLOC_TEST101", CarrierID: 1, Status: constants.IotCardStatusDistributed, ShopID: &shopID},
|
||||
{ICCID: "ALLOC_TEST102", CarrierID: 1, Status: constants.IotCardStatusDistributed, ShopID: &shopID},
|
||||
}
|
||||
for _, card := range cards {
|
||||
require.NoError(t, env.TX.Create(card).Error)
|
||||
}
|
||||
|
||||
t.Run("平台回收卡", func(t *testing.T) {
|
||||
reqBody := map[string]interface{}{
|
||||
"from_shop_id": shop.ID,
|
||||
"selection_type": "list",
|
||||
"iccids": []string{"ALLOC_TEST101"},
|
||||
"remark": "平台回收测试",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(reqBody)
|
||||
|
||||
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/iot-cards/standalone/recall", bodyBytes)
|
||||
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.RawDB().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 := integ.NewIntegrationTestEnv(t)
|
||||
|
||||
// 创建测试数据
|
||||
shop := env.CreateTestShop("测试店铺", 1, nil)
|
||||
|
||||
var superAdminAccount model.Account
|
||||
require.NoError(t, env.RawDB().Where("user_type = ?", constants.UserTypeSuperAdmin).First(&superAdminAccount).Error)
|
||||
|
||||
fromShopID := shop.ID
|
||||
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: shop.ID,
|
||||
OperatorID: superAdminAccount.ID,
|
||||
},
|
||||
{
|
||||
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: superAdminAccount.ID,
|
||||
},
|
||||
}
|
||||
for _, record := range records {
|
||||
require.NoError(t, env.TX.Create(record).Error)
|
||||
}
|
||||
|
||||
t.Run("获取分配记录列表", func(t *testing.T) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/asset-allocation-records?page=1&page_size=20", nil)
|
||||
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) {
|
||||
resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/asset-allocation-records?allocation_type=allocate", nil)
|
||||
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)
|
||||
resp, err := env.AsSuperAdmin().Request("GET", url, nil)
|
||||
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) {
|
||||
resp, err := env.ClearAuth().Request("GET", "/api/admin/asset-allocation-records", nil)
|
||||
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, "未认证请求应返回错误码")
|
||||
})
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
type EmailPayload struct {
|
||||
RequestID string `json:"request_id"`
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
CC []string `json:"cc,omitempty"`
|
||||
}
|
||||
|
||||
func getRedisOpt() asynq.RedisClientOpt {
|
||||
host := os.Getenv("JUNHONG_REDIS_ADDRESS")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
port := os.Getenv("JUNHONG_REDIS_PORT")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
password := os.Getenv("JUNHONG_REDIS_PASSWORD")
|
||||
return asynq.RedisClientOpt{
|
||||
Addr: host + ":" + port,
|
||||
Password: password,
|
||||
DB: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSubmit(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
_ = rdb
|
||||
|
||||
client := asynq.NewClient(getRedisOpt())
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
// 构造任务载荷
|
||||
payload := &EmailPayload{
|
||||
RequestID: "test-request-001",
|
||||
To: "test@example.com",
|
||||
Subject: "Test Email",
|
||||
Body: "This is a test email",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 提交任务
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task,
|
||||
asynq.Queue(constants.QueueDefault),
|
||||
asynq.MaxRetry(constants.DefaultRetryMax),
|
||||
)
|
||||
|
||||
// 验证
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, info.ID)
|
||||
assert.Equal(t, constants.QueueDefault, info.Queue)
|
||||
assert.Equal(t, constants.DefaultRetryMax, info.MaxRetry)
|
||||
}
|
||||
|
||||
func TestTaskPriority(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
client := asynq.NewClient(getRedisOpt())
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queue string
|
||||
}{
|
||||
{"Critical Priority", constants.QueueCritical},
|
||||
{"Default Priority", constants.QueueDefault},
|
||||
{"Low Priority", constants.QueueLow},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
payload := &EmailPayload{
|
||||
RequestID: "test-request-" + tt.queue,
|
||||
To: "test@example.com",
|
||||
Subject: "Test",
|
||||
Body: "Test",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task, asynq.Queue(tt.queue))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.queue, info.Queue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskRetry(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
client := asynq.NewClient(getRedisOpt())
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
payload := &EmailPayload{
|
||||
RequestID: "retry-test-001",
|
||||
To: "test@example.com",
|
||||
Subject: "Retry Test",
|
||||
Body: "Test retry mechanism",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 提交任务并设置重试次数
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
info, err := client.Enqueue(task,
|
||||
asynq.MaxRetry(3),
|
||||
asynq.Timeout(30*time.Second),
|
||||
)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, info.MaxRetry)
|
||||
assert.Equal(t, 30*time.Second, info.Timeout)
|
||||
}
|
||||
|
||||
func TestTaskIdempotency(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
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, "第一次设置锁应该成功")
|
||||
|
||||
// 第二次设置锁(模拟重复任务)
|
||||
result, err = rdb.SetNX(ctx, lockKey, "1", 24*time.Hour).Result()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result, "第二次设置锁应该失败(幂等性)")
|
||||
|
||||
// 验证锁存在
|
||||
exists, err := rdb.Exists(ctx, lockKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), exists)
|
||||
|
||||
// 验证 TTL
|
||||
ttl, err := rdb.TTL(ctx, lockKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, ttl.Hours(), 23.0)
|
||||
assert.LessOrEqual(t, ttl.Hours(), 24.0)
|
||||
}
|
||||
|
||||
func TestTaskStatusTracking(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
taskID := "task-123456"
|
||||
statusKey := constants.RedisTaskStatusKey(taskID)
|
||||
|
||||
// 设置任务状态
|
||||
statuses := []string{"pending", "processing", "completed"}
|
||||
|
||||
for _, status := range statuses {
|
||||
err := rdb.Set(ctx, statusKey, status, 7*24*time.Hour).Err()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 读取状态
|
||||
result, err := rdb.Get(ctx, statusKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, status, result)
|
||||
}
|
||||
|
||||
// 验证 TTL
|
||||
ttl, err := rdb.TTL(ctx, statusKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, ttl.Hours(), 24.0*6)
|
||||
}
|
||||
|
||||
func TestQueueInspection(t *testing.T) {
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
inspector := asynq.NewInspector(getRedisOpt())
|
||||
defer func() { _ = inspector.Close() }()
|
||||
|
||||
_, _ = inspector.DeleteAllPendingTasks(constants.QueueDefault)
|
||||
_, _ = inspector.DeleteAllScheduledTasks(constants.QueueDefault)
|
||||
_, _ = inspector.DeleteAllRetryTasks(constants.QueueDefault)
|
||||
_, _ = inspector.DeleteAllArchivedTasks(constants.QueueDefault)
|
||||
|
||||
client := asynq.NewClient(getRedisOpt())
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
payload := &EmailPayload{
|
||||
RequestID: "test-" + string(rune(i)),
|
||||
To: "test@example.com",
|
||||
Subject: "Test",
|
||||
Body: "Test",
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
task := asynq.NewTask(constants.TaskTypeEmailSend, payloadBytes)
|
||||
_, err = client.Enqueue(task, asynq.Queue(constants.QueueDefault))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
info, err := inspector.GetQueueInfo(constants.QueueDefault)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5, info.Pending)
|
||||
assert.Equal(t, 0, info.Active)
|
||||
}
|
||||
|
||||
func TestTaskSerialization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload EmailPayload
|
||||
}{
|
||||
{
|
||||
name: "Simple Email",
|
||||
payload: EmailPayload{
|
||||
RequestID: "req-001",
|
||||
To: "user@example.com",
|
||||
Subject: "Hello",
|
||||
Body: "Hello World",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Email with CC",
|
||||
payload: EmailPayload{
|
||||
RequestID: "req-002",
|
||||
To: "user@example.com",
|
||||
Subject: "Hello",
|
||||
Body: "Hello World",
|
||||
CC: []string{"cc1@example.com", "cc2@example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
assert.Equal(t, tt.payload.Body, decoded.Body)
|
||||
assert.Equal(t, tt.payload.CC, decoded.CC)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/auth"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// phoneCounter 用于生成唯一的手机号
|
||||
var phoneCounter uint64
|
||||
|
||||
func init() {
|
||||
// 使用当前时间作为随机种子
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
// 初始化计数器为一个随机值,避免不同测试运行之间的冲突
|
||||
phoneCounter = uint64(rand.Intn(10000))
|
||||
}
|
||||
|
||||
// GenerateUniquePhone 生成唯一的测试手机号(导出供测试使用)
|
||||
func GenerateUniquePhone() string {
|
||||
counter := atomic.AddUint64(&phoneCounter, 1)
|
||||
timestamp := time.Now().UnixNano() % 10000
|
||||
return fmt.Sprintf("139%04d%04d", timestamp, counter%10000)
|
||||
}
|
||||
|
||||
// CreateTestAccount 创建测试账号
|
||||
// userType: 1=超级管理员, 2=平台用户, 3=代理账号, 4=企业账号
|
||||
func CreateTestAccount(t *testing.T, db *gorm.DB, username, password string, userType int, shopID, enterpriseID *uint) *model.Account {
|
||||
t.Helper()
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
|
||||
phone := GenerateUniquePhone()
|
||||
|
||||
account := &model.Account{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
Username: username,
|
||||
Phone: phone,
|
||||
Password: string(hashedPassword),
|
||||
UserType: userType,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err = db.Create(account).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
// GenerateTestToken 为测试账号生成 token
|
||||
func GenerateTestToken(t *testing.T, rdb *redis.Client, account *model.Account, device string) (accessToken, refreshToken string) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var shopID, enterpriseID uint
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
}
|
||||
if account.EnterpriseID != nil {
|
||||
enterpriseID = *account.EnterpriseID
|
||||
}
|
||||
|
||||
tokenInfo := &auth.TokenInfo{
|
||||
UserID: account.ID,
|
||||
UserType: account.UserType,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
Username: account.Username,
|
||||
Device: device,
|
||||
IP: "127.0.0.1",
|
||||
}
|
||||
|
||||
tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour)
|
||||
accessToken, refreshToken, err := tokenManager.GenerateTokenPair(ctx, tokenInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
return accessToken, refreshToken
|
||||
}
|
||||
|
||||
// usernameCounter 用于生成唯一的用户名
|
||||
var usernameCounter uint64
|
||||
|
||||
func init() {
|
||||
usernameCounter = uint64(rand.Intn(100000))
|
||||
}
|
||||
|
||||
// GenerateUniqueUsername 生成唯一的测试用户名(导出供测试使用)
|
||||
func GenerateUniqueUsername(prefix string) string {
|
||||
counter := atomic.AddUint64(&usernameCounter, 1)
|
||||
return fmt.Sprintf("%s_%d", prefix, counter)
|
||||
}
|
||||
|
||||
// CreateSuperAdmin 创建或获取超级管理员测试账号
|
||||
func CreateSuperAdmin(t *testing.T, db *gorm.DB) *model.Account {
|
||||
t.Helper()
|
||||
|
||||
var existing model.Account
|
||||
err := db.Where("user_type = ?", constants.UserTypeSuperAdmin).First(&existing).Error
|
||||
if err == nil {
|
||||
return &existing
|
||||
}
|
||||
|
||||
return CreateTestAccount(t, db, GenerateUniqueUsername("superadmin"), "password123", constants.UserTypeSuperAdmin, nil, nil)
|
||||
}
|
||||
|
||||
// CreatePlatformUser 创建平台用户测试账号
|
||||
func CreatePlatformUser(t *testing.T, db *gorm.DB) *model.Account {
|
||||
t.Helper()
|
||||
return CreateTestAccount(t, db, GenerateUniqueUsername("platformuser"), "password123", constants.UserTypePlatform, nil, nil)
|
||||
}
|
||||
|
||||
// CreateAgentUser 创建代理账号测试账号
|
||||
func CreateAgentUser(t *testing.T, db *gorm.DB, shopID uint) *model.Account {
|
||||
t.Helper()
|
||||
return CreateTestAccount(t, db, GenerateUniqueUsername("agentuser"), "password123", constants.UserTypeAgent, &shopID, nil)
|
||||
}
|
||||
|
||||
// CreateEnterpriseUser 创建企业账号测试账号
|
||||
func CreateEnterpriseUser(t *testing.T, db *gorm.DB, enterpriseID uint) *model.Account {
|
||||
t.Helper()
|
||||
return CreateTestAccount(t, db, GenerateUniqueUsername("enterpriseuser"), "password123", constants.UserTypeEnterprise, nil, &enterpriseID)
|
||||
}
|
||||
|
||||
// shopCodeCounter 用于生成唯一的商户代码
|
||||
var shopCodeCounter uint64
|
||||
|
||||
// CreateTestShop 创建测试商户
|
||||
func CreateTestShop(t *testing.T, db *gorm.DB, name, code string, level int, parentID *uint) *model.Shop {
|
||||
t.Helper()
|
||||
|
||||
counter := atomic.AddUint64(&shopCodeCounter, 1)
|
||||
uniqueCode := fmt.Sprintf("%s_%d_%d", code, time.Now().UnixNano()%10000, counter)
|
||||
uniqueName := fmt.Sprintf("%s_%d", name, counter)
|
||||
|
||||
shop := &model.Shop{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopName: uniqueName,
|
||||
ShopCode: uniqueCode,
|
||||
Level: level,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if parentID != nil {
|
||||
shop.ParentID = parentID
|
||||
}
|
||||
|
||||
err := db.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
return shop
|
||||
}
|
||||
|
||||
// SetupAuthMiddleware 设置认证中间件(用于集成测试)
|
||||
func SetupAuthMiddleware(t *testing.T, tokenManager *auth.TokenManager, allowedUserTypes []int) func(token string) bool {
|
||||
t.Helper()
|
||||
|
||||
return func(token string) bool {
|
||||
ctx := context.Background()
|
||||
tokenInfo, err := tokenManager.ValidateAccessToken(ctx, token)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查用户类型
|
||||
if len(allowedUserTypes) > 0 {
|
||||
allowed := false
|
||||
for _, userType := range allowedUserTypes {
|
||||
if tokenInfo.UserType == userType {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
)
|
||||
|
||||
// 全局单例数据库和 Redis 连接
|
||||
// 使用 sync.Once 确保整个测试套件只创建一次连接,显著提升测试性能
|
||||
var (
|
||||
testDBOnce sync.Once
|
||||
testDB *gorm.DB
|
||||
testDBInitErr error
|
||||
|
||||
testRedisOnce sync.Once
|
||||
testRedis *redis.Client
|
||||
testRedisInitErr error
|
||||
)
|
||||
|
||||
// 测试数据库配置
|
||||
// TODO: 未来可以从环境变量或配置文件加载
|
||||
const (
|
||||
testDBDSN = "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
|
||||
testRedisAddr = "cxd.whcxd.cn:16299"
|
||||
testRedisPasswd = "cpNbWtAaqgo1YJmbMp3h"
|
||||
testRedisDB = 15
|
||||
)
|
||||
|
||||
// GetTestDB 获取全局单例测试数据库连接
|
||||
//
|
||||
// 特点:
|
||||
// - 使用 sync.Once 确保整个测试套件只创建一次连接
|
||||
// - AutoMigrate 只在首次连接时执行一次
|
||||
// - 连接失败会跳过测试(不是致命错误)
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// db := testutils.GetTestDB(t)
|
||||
// // db 是全局共享的连接,不要直接修改其状态
|
||||
// // 如需事务隔离,使用 NewTestTransaction(t)
|
||||
// }
|
||||
func GetTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
testDBOnce.Do(func() {
|
||||
var err error
|
||||
testDB, err = gorm.Open(postgres.Open(testDBDSN), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
testDBInitErr = fmt.Errorf("无法连接测试数据库: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = testDB.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.Role{},
|
||||
&model.Permission{},
|
||||
&model.AccountRole{},
|
||||
&model.RolePermission{},
|
||||
&model.Shop{},
|
||||
&model.Enterprise{},
|
||||
&model.PersonalCustomer{},
|
||||
&model.PersonalCustomerPhone{},
|
||||
&model.PersonalCustomerICCID{},
|
||||
&model.PersonalCustomerDevice{},
|
||||
&model.IotCard{},
|
||||
&model.IotCardImportTask{},
|
||||
&model.Device{},
|
||||
&model.DeviceImportTask{},
|
||||
&model.DeviceSimBinding{},
|
||||
&model.Carrier{},
|
||||
&model.Tag{},
|
||||
&model.PackageSeries{},
|
||||
&model.Package{},
|
||||
&model.ShopPackageAllocation{},
|
||||
&model.EnterpriseCardAuthorization{},
|
||||
&model.EnterpriseDeviceAuthorization{},
|
||||
&model.AssetAllocationRecord{},
|
||||
&model.CommissionWithdrawalRequest{},
|
||||
&model.CommissionWithdrawalSetting{},
|
||||
&model.Order{},
|
||||
&model.OrderItem{},
|
||||
&model.PackageUsage{},
|
||||
&model.Wallet{},
|
||||
)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "does not exist") && (strings.Contains(errMsg, "constraint") || strings.Contains(errMsg, "column")) {
|
||||
// 忽略约束和列不存在的错误,这是由于约束名变更或迁移未应用导致的
|
||||
} else {
|
||||
testDBInitErr = fmt.Errorf("数据库迁移失败: %w", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有必要的列都存在(处理迁移未应用的情况)
|
||||
ensureTestDBColumns(testDB)
|
||||
})
|
||||
|
||||
if testDBInitErr != nil {
|
||||
t.Skipf("跳过测试:%v", testDBInitErr)
|
||||
}
|
||||
|
||||
return testDB
|
||||
}
|
||||
|
||||
// GetTestRedis 获取全局单例 Redis 连接
|
||||
//
|
||||
// 特点:
|
||||
// - 使用 sync.Once 确保整个测试套件只创建一次连接
|
||||
// - 连接失败会跳过测试(不是致命错误)
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// rdb := testutils.GetTestRedis(t)
|
||||
// // rdb 是全局共享的连接
|
||||
// // 使用 CleanTestRedisKeys(t) 自动清理测试相关的 Redis 键
|
||||
// }
|
||||
func GetTestRedis(t *testing.T) *redis.Client {
|
||||
t.Helper()
|
||||
|
||||
testRedisOnce.Do(func() {
|
||||
testRedis = redis.NewClient(&redis.Options{
|
||||
Addr: testRedisAddr,
|
||||
Password: testRedisPasswd,
|
||||
DB: testRedisDB,
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
if err := testRedis.Ping(ctx).Err(); err != nil {
|
||||
testRedisInitErr = fmt.Errorf("无法连接 Redis: %w", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if testRedisInitErr != nil {
|
||||
t.Skipf("跳过测试:%v", testRedisInitErr)
|
||||
}
|
||||
|
||||
return testRedis
|
||||
}
|
||||
|
||||
// NewTestTransaction 创建测试事务,自动在测试结束时回滚
|
||||
//
|
||||
// 特点:
|
||||
// - 每个测试用例获得独立的事务,互不干扰
|
||||
// - 使用 t.Cleanup() 确保即使测试 panic 也能回滚
|
||||
// - 回滚后数据库状态与测试前完全一致
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// tx := testutils.NewTestTransaction(t)
|
||||
// // 所有数据库操作使用 tx 而非 db
|
||||
// store := postgres.NewXxxStore(tx, rdb)
|
||||
// // 测试结束后自动回滚,无需手动清理
|
||||
// }
|
||||
//
|
||||
// 注意:
|
||||
// - 不要在子测试(t.Run)中调用此函数,因为子测试可能并行执行
|
||||
// - 如需在子测试中使用数据库,应在父测试中创建事务并传递
|
||||
func NewTestTransaction(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db := GetTestDB(t)
|
||||
// 确保所有必要的列都存在
|
||||
ensureTestDBColumns(db)
|
||||
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
t.Fatalf("开启测试事务失败: %v", tx.Error)
|
||||
}
|
||||
|
||||
// 使用 t.Cleanup() 确保测试结束时自动回滚
|
||||
// 即使测试 panic 也能执行清理
|
||||
t.Cleanup(func() {
|
||||
tx.Rollback()
|
||||
})
|
||||
|
||||
return tx
|
||||
}
|
||||
|
||||
// CleanTestRedisKeys 清理当前测试的 Redis 键
|
||||
//
|
||||
// 特点:
|
||||
// - 使用测试名称作为键前缀,格式: test:{TestName}:*
|
||||
// - 测试开始时清理已有键(防止脏数据)
|
||||
// - 使用 t.Cleanup() 确保测试结束时自动清理
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// rdb := testutils.GetTestRedis(t)
|
||||
// testutils.CleanTestRedisKeys(t, rdb)
|
||||
// // Redis 键使用测试专用前缀: test:TestXxx:your_key
|
||||
// }
|
||||
//
|
||||
// 键命名规范:
|
||||
// - 测试中创建的键应使用 GetTestRedisKeyPrefix(t) 作为前缀
|
||||
// - 例如: test:TestShopStore_Create:cache:shop:1
|
||||
func CleanTestRedisKeys(t *testing.T, rdb *redis.Client) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
testPrefix := GetTestRedisKeyPrefix(t)
|
||||
|
||||
// 测试开始前清理已有键
|
||||
cleanKeys(ctx, rdb, testPrefix)
|
||||
|
||||
// 测试结束时自动清理
|
||||
t.Cleanup(func() {
|
||||
cleanKeys(ctx, rdb, testPrefix)
|
||||
})
|
||||
}
|
||||
|
||||
// GetTestRedisKeyPrefix 获取当前测试的 Redis 键前缀
|
||||
//
|
||||
// 返回格式: test:{TestName}:
|
||||
// 用于在测试中创建带前缀的 Redis 键,确保键不会与其他测试冲突
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// prefix := testutils.GetTestRedisKeyPrefix(t)
|
||||
// key := prefix + "my_cache_key"
|
||||
// // key = "test:TestXxx:my_cache_key"
|
||||
// }
|
||||
func GetTestRedisKeyPrefix(t *testing.T) string {
|
||||
t.Helper()
|
||||
return fmt.Sprintf("test:%s:", t.Name())
|
||||
}
|
||||
|
||||
// cleanKeys 清理匹配前缀的所有 Redis 键
|
||||
func cleanKeys(ctx context.Context, rdb *redis.Client, prefix string) {
|
||||
keys, err := rdb.Keys(ctx, prefix+"*").Result()
|
||||
if err != nil {
|
||||
// 忽略 Redis 错误,不影响测试
|
||||
return
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
rdb.Del(ctx, keys...)
|
||||
}
|
||||
}
|
||||
|
||||
// ensureTestDBColumns 确保测试数据库中所有必要的列都存在
|
||||
// 处理迁移未应用导致的列缺失问题
|
||||
func ensureTestDBColumns(db *gorm.DB) {
|
||||
// 添加 force_recharge_trigger_type 列到 tb_shop_series_allocation 表
|
||||
if !db.Migrator().HasColumn("tb_shop_series_allocation", "force_recharge_trigger_type") {
|
||||
db.Exec("ALTER TABLE tb_shop_series_allocation ADD COLUMN force_recharge_trigger_type int DEFAULT 2")
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// GetMigrationsPath 获取数据库迁移文件的路径
|
||||
func GetMigrationsPath() string {
|
||||
_, filename, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
panic("无法获取当前文件路径")
|
||||
}
|
||||
|
||||
projectRoot := filepath.Join(filepath.Dir(filename), "..", "..")
|
||||
migrationsPath := filepath.Join(projectRoot, "migrations")
|
||||
|
||||
return migrationsPath
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
package integ
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/bootstrap"
|
||||
"github.com/break/junhong_cmp_fiber/internal/gateway"
|
||||
"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"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// IntegrationTestEnv 集成测试环境
|
||||
// 封装集成测试所需的所有依赖,提供统一的测试环境设置
|
||||
type IntegrationTestEnv struct {
|
||||
TX *gorm.DB // 自动回滚的数据库事务
|
||||
Redis *redis.Client // 全局 Redis 连接
|
||||
Logger *zap.Logger // 测试用日志记录器
|
||||
TokenManager *auth.TokenManager // Token 管理器
|
||||
App *fiber.App // 配置好的 Fiber 应用实例
|
||||
Handlers *bootstrap.Handlers
|
||||
Middlewares *bootstrap.Middlewares
|
||||
|
||||
t *testing.T
|
||||
superAdmin *model.Account
|
||||
currentToken string
|
||||
}
|
||||
|
||||
// NewIntegrationTestEnv 创建集成测试环境
|
||||
//
|
||||
// 自动完成以下初始化:
|
||||
// - 创建独立的数据库事务(测试结束后自动回滚)
|
||||
// - 获取全局 Redis 连接并清理测试键
|
||||
// - 创建 Logger 和 TokenManager
|
||||
// - 通过 Bootstrap 初始化所有 Handlers 和 Middlewares
|
||||
// - 配置 Fiber App 并注册路由
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// func TestXxx(t *testing.T) {
|
||||
// env := testutils.NewIntegrationTestEnv(t)
|
||||
// // env.App 已配置好,可直接发送请求
|
||||
// // env.TX 是独立事务,测试结束后自动回滚
|
||||
// }
|
||||
var configOnce sync.Once
|
||||
|
||||
func NewIntegrationTestEnv(t *testing.T) *IntegrationTestEnv {
|
||||
t.Helper()
|
||||
|
||||
configOnce.Do(func() {
|
||||
_, _ = config.Load()
|
||||
})
|
||||
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
tokenManager := auth.NewTokenManager(rdb, 24*time.Hour, 7*24*time.Hour)
|
||||
|
||||
gatewayClient := createMockGatewayClient()
|
||||
|
||||
deps := &bootstrap.Dependencies{
|
||||
DB: tx,
|
||||
Redis: rdb,
|
||||
Logger: logger,
|
||||
TokenManager: tokenManager,
|
||||
GatewayClient: gatewayClient,
|
||||
}
|
||||
|
||||
result, err := bootstrap.Bootstrap(deps)
|
||||
require.NoError(t, err, "Bootstrap 初始化失败")
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
ErrorHandler: errors.SafeErrorHandler(logger),
|
||||
})
|
||||
|
||||
routes.RegisterRoutes(app, result.Handlers, result.Middlewares)
|
||||
|
||||
env := &IntegrationTestEnv{
|
||||
TX: tx,
|
||||
Redis: rdb,
|
||||
Logger: logger,
|
||||
TokenManager: tokenManager,
|
||||
App: app,
|
||||
Handlers: result.Handlers,
|
||||
Middlewares: result.Middlewares,
|
||||
t: t,
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
func createMockGatewayClient() *gateway.Client {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
resp := gateway.GatewayResponse{
|
||||
Code: 200,
|
||||
Msg: "success",
|
||||
TraceID: "test-trace-id",
|
||||
Data: json.RawMessage(`{}`),
|
||||
}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
|
||||
client := gateway.NewClient(server.URL, "test-app-id", "test-app-secret")
|
||||
return client
|
||||
}
|
||||
|
||||
// AsSuperAdmin 设置当前请求使用超级管理员身份
|
||||
// 返回 IntegrationTestEnv 以支持链式调用
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles", nil)
|
||||
func (e *IntegrationTestEnv) AsSuperAdmin() *IntegrationTestEnv {
|
||||
e.t.Helper()
|
||||
|
||||
if e.superAdmin == nil {
|
||||
e.superAdmin = e.ensureSuperAdmin()
|
||||
}
|
||||
|
||||
e.currentToken = e.generateToken(e.superAdmin)
|
||||
return e
|
||||
}
|
||||
|
||||
// AsUser 设置当前请求使用指定用户身份
|
||||
// 返回 IntegrationTestEnv 以支持链式调用
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// account := e.CreateTestAccount(...)
|
||||
// resp, err := env.AsUser(account).Request("GET", "/api/admin/shops", nil)
|
||||
func (e *IntegrationTestEnv) AsUser(account *model.Account) *IntegrationTestEnv {
|
||||
e.t.Helper()
|
||||
|
||||
token := e.generateToken(account)
|
||||
e.currentToken = token
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Request 发送 HTTP 请求
|
||||
// 自动添加 Authorization header(如果已设置用户身份)
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// resp, err := env.AsSuperAdmin().Request("GET", "/api/admin/roles", nil)
|
||||
// resp, err := env.Request("POST", "/api/admin/login", loginBody)
|
||||
func (e *IntegrationTestEnv) Request(method, path string, body []byte) (*http.Response, error) {
|
||||
e.t.Helper()
|
||||
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
req = httptest.NewRequest(method, path, bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
if e.currentToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+e.currentToken)
|
||||
}
|
||||
|
||||
return e.App.Test(req, -1)
|
||||
}
|
||||
|
||||
// RequestWithHeaders 发送带自定义 Headers 的 HTTP 请求
|
||||
func (e *IntegrationTestEnv) RequestWithHeaders(method, path string, body []byte, headers map[string]string) (*http.Response, error) {
|
||||
e.t.Helper()
|
||||
|
||||
var req *http.Request
|
||||
if body != nil {
|
||||
req = httptest.NewRequest(method, path, bytes.NewReader(body))
|
||||
} else {
|
||||
req = httptest.NewRequest(method, path, nil)
|
||||
}
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
if body != nil && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
if e.currentToken != "" && req.Header.Get("Authorization") == "" {
|
||||
req.Header.Set("Authorization", "Bearer "+e.currentToken)
|
||||
}
|
||||
|
||||
return e.App.Test(req, -1)
|
||||
}
|
||||
|
||||
// ClearAuth 清除当前认证状态
|
||||
func (e *IntegrationTestEnv) ClearAuth() *IntegrationTestEnv {
|
||||
e.currentToken = ""
|
||||
return e
|
||||
}
|
||||
|
||||
// ensureSuperAdmin 确保超级管理员账号存在
|
||||
func (e *IntegrationTestEnv) ensureSuperAdmin() *model.Account {
|
||||
e.t.Helper()
|
||||
|
||||
var existing model.Account
|
||||
err := e.TX.Where("user_type = ?", constants.UserTypeSuperAdmin).First(&existing).Error
|
||||
if err == nil {
|
||||
return &existing
|
||||
}
|
||||
|
||||
return e.CreateTestAccount("superadmin", "password123", constants.UserTypeSuperAdmin, nil, nil)
|
||||
}
|
||||
|
||||
// generateToken 为账号生成访问 Token
|
||||
func (e *IntegrationTestEnv) generateToken(account *model.Account) string {
|
||||
e.t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var shopID, enterpriseID uint
|
||||
if account.ShopID != nil {
|
||||
shopID = *account.ShopID
|
||||
}
|
||||
if account.EnterpriseID != nil {
|
||||
enterpriseID = *account.EnterpriseID
|
||||
}
|
||||
|
||||
tokenInfo := &auth.TokenInfo{
|
||||
UserID: account.ID,
|
||||
UserType: account.UserType,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
Username: account.Username,
|
||||
Device: "test",
|
||||
IP: "127.0.0.1",
|
||||
}
|
||||
|
||||
accessToken, _, err := e.TokenManager.GenerateTokenPair(ctx, tokenInfo)
|
||||
require.NoError(e.t, err, "生成 Token 失败")
|
||||
|
||||
return accessToken
|
||||
}
|
||||
|
||||
var (
|
||||
usernameCounter uint64
|
||||
phoneCounter uint64
|
||||
shopCodeCounter uint64
|
||||
)
|
||||
|
||||
// CreateTestAccount 创建测试账号
|
||||
func (e *IntegrationTestEnv) CreateTestAccount(username, password string, userType int, shopID, enterpriseID *uint) *model.Account {
|
||||
e.t.Helper()
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
require.NoError(e.t, err)
|
||||
|
||||
counter := atomic.AddUint64(&usernameCounter, 1)
|
||||
uniqueUsername := fmt.Sprintf("%s_%d", username, counter)
|
||||
uniquePhone := fmt.Sprintf("138%08d", atomic.AddUint64(&phoneCounter, 1))
|
||||
|
||||
account := &model.Account{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
Username: uniqueUsername,
|
||||
Phone: uniquePhone,
|
||||
Password: string(hashedPassword),
|
||||
UserType: userType,
|
||||
ShopID: shopID,
|
||||
EnterpriseID: enterpriseID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err = e.TX.Create(account).Error
|
||||
require.NoError(e.t, err, "创建测试账号失败")
|
||||
|
||||
return account
|
||||
}
|
||||
|
||||
// CreateTestShop 创建测试商户
|
||||
func (e *IntegrationTestEnv) CreateTestShop(name string, level int, parentID *uint) *model.Shop {
|
||||
e.t.Helper()
|
||||
|
||||
counter := atomic.AddUint64(&shopCodeCounter, 1)
|
||||
uniqueCode := fmt.Sprintf("SHOP_%d_%d", time.Now().UnixNano()%10000, counter)
|
||||
uniqueName := fmt.Sprintf("%s_%d", name, counter)
|
||||
|
||||
shop := &model.Shop{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
ShopName: uniqueName,
|
||||
ShopCode: uniqueCode,
|
||||
Level: level,
|
||||
ParentID: parentID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err := e.TX.Create(shop).Error
|
||||
require.NoError(e.t, err, "创建测试商户失败")
|
||||
|
||||
return shop
|
||||
}
|
||||
|
||||
// CreateTestEnterprise 创建测试企业
|
||||
func (e *IntegrationTestEnv) CreateTestEnterprise(name string, ownerShopID *uint) *model.Enterprise {
|
||||
e.t.Helper()
|
||||
|
||||
counter := atomic.AddUint64(&shopCodeCounter, 1)
|
||||
uniqueCode := fmt.Sprintf("ENT_%d_%d", time.Now().UnixNano()%10000, counter)
|
||||
uniqueName := fmt.Sprintf("%s_%d", name, counter)
|
||||
|
||||
enterprise := &model.Enterprise{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
EnterpriseName: uniqueName,
|
||||
EnterpriseCode: uniqueCode,
|
||||
OwnerShopID: ownerShopID,
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
err := e.TX.Create(enterprise).Error
|
||||
require.NoError(e.t, err, "创建测试企业失败")
|
||||
|
||||
return enterprise
|
||||
}
|
||||
|
||||
// CreateTestRole 创建测试角色
|
||||
func (e *IntegrationTestEnv) CreateTestRole(name string, roleType int) *model.Role {
|
||||
e.t.Helper()
|
||||
|
||||
counter := atomic.AddUint64(&usernameCounter, 1)
|
||||
uniqueName := fmt.Sprintf("%s_%d", name, counter)
|
||||
|
||||
role := &model.Role{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
RoleName: uniqueName,
|
||||
RoleType: roleType,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := e.TX.Create(role).Error
|
||||
require.NoError(e.t, err, "创建测试角色失败")
|
||||
|
||||
return role
|
||||
}
|
||||
|
||||
// CreateTestPermission 创建测试权限
|
||||
func (e *IntegrationTestEnv) CreateTestPermission(name, code string, permType int) *model.Permission {
|
||||
e.t.Helper()
|
||||
|
||||
counter := atomic.AddUint64(&usernameCounter, 1)
|
||||
uniqueName := fmt.Sprintf("%s_%d", name, counter)
|
||||
uniqueCode := fmt.Sprintf("%s_%d", code, counter)
|
||||
|
||||
permission := &model.Permission{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
PermName: uniqueName,
|
||||
PermCode: uniqueCode,
|
||||
PermType: permType,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := e.TX.Create(permission).Error
|
||||
require.NoError(e.t, err, "创建测试权限失败")
|
||||
|
||||
return permission
|
||||
}
|
||||
|
||||
// SetUserContext 设置用户上下文(用于直接调用 Service 层测试)
|
||||
func (e *IntegrationTestEnv) SetUserContext(ctx context.Context, userID uint, userType int, shopID uint) context.Context {
|
||||
return middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(userID, userType, shopID))
|
||||
}
|
||||
|
||||
// GetSuperAdminContext 获取超级管理员上下文
|
||||
func (e *IntegrationTestEnv) GetSuperAdminContext() context.Context {
|
||||
if e.superAdmin == nil {
|
||||
e.superAdmin = e.ensureSuperAdmin()
|
||||
}
|
||||
return e.SetUserContext(context.Background(), e.superAdmin.ID, constants.UserTypeSuperAdmin, 0)
|
||||
}
|
||||
|
||||
// RawDB 获取跳过数据权限过滤的数据库连接
|
||||
// 用于测试中验证数据是否正确写入,不受 GORM Callback 影响
|
||||
//
|
||||
// 用法:
|
||||
//
|
||||
// var count int64
|
||||
// env.RawDB().Model(&model.Role{}).Where("role_name = ?", name).Count(&count)
|
||||
func (e *IntegrationTestEnv) RawDB() *gorm.DB {
|
||||
ctx := e.GetSuperAdminContext()
|
||||
return e.TX.WithContext(ctx)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package testutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateUsername 生成测试用户名
|
||||
func GenerateUsername(prefix string, index int) string {
|
||||
return fmt.Sprintf("%s_%d", prefix, index)
|
||||
}
|
||||
|
||||
// GeneratePhone 生成测试手机号
|
||||
func GeneratePhone(prefix string, index int) string {
|
||||
return fmt.Sprintf("%s%08d", prefix, index)
|
||||
}
|
||||
|
||||
// GenerateUniquePhone 生成唯一手机号(基于时间戳)
|
||||
func GenerateUniquePhone() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
suffix := timestamp % 100000000
|
||||
return fmt.Sprintf("138%08d", suffix)
|
||||
}
|
||||
|
||||
// Now 返回当前时间
|
||||
func Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// Since 返回从指定时间到现在的持续时间
|
||||
func Since(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestAccountModel_Create 测试创建账号
|
||||
func TestAccountModel_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("创建 root 账号", func(t *testing.T) {
|
||||
account := &model.Account{
|
||||
Username: "root_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypeSuperAdmin,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, account.ID)
|
||||
assert.NotZero(t, account.CreatedAt)
|
||||
assert.NotZero(t, account.UpdatedAt)
|
||||
})
|
||||
|
||||
// 注意:parent_id 字段已被移除,层级关系通过 shop_id 和 enterprise_id 维护
|
||||
|
||||
t.Run("创建带 shop_id 的账号", func(t *testing.T) {
|
||||
shopID := uint(100)
|
||||
account := &model.Account{
|
||||
Username: "shop_user",
|
||||
Phone: "13800000004",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
ShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, account.ShopID)
|
||||
assert.Equal(t, uint(100), *account.ShopID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_GetByID 测试根据 ID 查询账号
|
||||
func TestAccountModel_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "test_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询存在的账号", func(t *testing.T) {
|
||||
found, err := store.GetByID(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.Username, found.Username)
|
||||
assert.Equal(t, account.Phone, found.Phone)
|
||||
assert.Equal(t, account.UserType, found.UserType)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的账号", func(t *testing.T) {
|
||||
_, err := store.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_GetByUsername 测试根据用户名查询账号
|
||||
func TestAccountModel_GetByUsername(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "unique_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据用户名查询", func(t *testing.T) {
|
||||
found, err := store.GetByUsername(ctx, "unique_user")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.ID, found.ID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的用户名", func(t *testing.T) {
|
||||
_, err := store.GetByUsername(ctx, "nonexistent")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_GetByPhone 测试根据手机号查询账号
|
||||
func TestAccountModel_GetByPhone(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "phone_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据手机号查询", func(t *testing.T) {
|
||||
found, err := store.GetByPhone(ctx, "13800000001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.ID, found.ID)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的手机号", func(t *testing.T) {
|
||||
_, err := store.GetByPhone(ctx, "99900000000")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_Update 测试更新账号
|
||||
func TestAccountModel_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "update_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新账号状态", func(t *testing.T) {
|
||||
account.Status = constants.StatusDisabled
|
||||
account.Updater = 2
|
||||
err := store.Update(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.GetByID(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, found.Status)
|
||||
assert.Equal(t, uint(2), found.Updater)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_List 测试查询账号列表
|
||||
func TestAccountModel_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建多个测试账号
|
||||
for i := 1; i <= 5; i++ {
|
||||
account := &model.Account{
|
||||
Username: testutils.GenerateUsername("list_user", i),
|
||||
Phone: testutils.GeneratePhone("138", i),
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
accounts, total, err := store.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(accounts), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
|
||||
t.Run("带过滤条件查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"user_type": constants.UserTypePlatform,
|
||||
}
|
||||
accounts, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
for _, acc := range accounts {
|
||||
assert.Equal(t, constants.UserTypePlatform, acc.UserType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountModel_UniqueConstraints 测试唯一约束
|
||||
func TestAccountModel_UniqueConstraints(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "unique_test",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("重复用户名应失败", func(t *testing.T) {
|
||||
duplicate := &model.Account{
|
||||
Username: "unique_test", // 重复
|
||||
Phone: "13800000002",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, duplicate)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("重复手机号应失败", func(t *testing.T) {
|
||||
duplicate := &model.Account{
|
||||
Username: "unique_test2",
|
||||
Phone: "13800000001", // 重复
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, duplicate)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createWithdrawalTestContext(userID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalService_ListWithdrawalRequests(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal.New(tx, shopStore, accountStore, walletStore, walletTransactionStore, commissionWithdrawalRequestStore)
|
||||
|
||||
t.Run("查询提现申请列表-空结果", func(t *testing.T) {
|
||||
ctx := createWithdrawalTestContext(1)
|
||||
|
||||
req := &dto.WithdrawalRequestListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListWithdrawalRequests(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("按状态筛选提现申请", func(t *testing.T) {
|
||||
ctx := createWithdrawalTestContext(1)
|
||||
|
||||
status := 1
|
||||
req := &dto.WithdrawalRequestListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
Status: &status,
|
||||
}
|
||||
|
||||
result, err := service.ListWithdrawalRequests(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("按时间范围筛选提现申请", func(t *testing.T) {
|
||||
ctx := createWithdrawalTestContext(1)
|
||||
|
||||
req := &dto.WithdrawalRequestListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
StartTime: "2025-01-01 00:00:00",
|
||||
EndTime: "2025-12-31 23:59:59",
|
||||
}
|
||||
|
||||
result, err := service.ListWithdrawalRequests(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalService_Approve(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal.New(tx, shopStore, accountStore, walletStore, walletTransactionStore, commissionWithdrawalRequestStore)
|
||||
|
||||
t.Run("审批不存在的提现申请应失败", func(t *testing.T) {
|
||||
ctx := createWithdrawalTestContext(1)
|
||||
|
||||
req := &dto.ApproveWithdrawalReq{
|
||||
PaymentType: "manual",
|
||||
}
|
||||
|
||||
_, err := service.Approve(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalService_Reject(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal.New(tx, shopStore, accountStore, walletStore, walletTransactionStore, commissionWithdrawalRequestStore)
|
||||
|
||||
t.Run("拒绝不存在的提现申请应失败", func(t *testing.T) {
|
||||
ctx := createWithdrawalTestContext(1)
|
||||
|
||||
req := &dto.RejectWithdrawalReq{
|
||||
Remark: "测试拒绝原因",
|
||||
}
|
||||
|
||||
_, err := service.Reject(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalService_ConcurrentApproval(t *testing.T) {
|
||||
t.Run("并发审批测试-状态检查", func(t *testing.T) {
|
||||
assert.True(t, true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalService_InsufficientBalance(t *testing.T) {
|
||||
t.Run("余额不足测试", func(t *testing.T) {
|
||||
assert.True(t, true)
|
||||
})
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/commission_withdrawal_setting"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createWithdrawalSettingTestContext(userID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalSettingService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal_setting.New(tx, accountStore, settingStore)
|
||||
|
||||
t.Run("新增提现配置", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
req := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: 5,
|
||||
MinWithdrawalAmount: 10000,
|
||||
FeeRate: 100,
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 5, result.DailyWithdrawalLimit)
|
||||
assert.Equal(t, int64(10000), result.MinWithdrawalAmount)
|
||||
assert.Equal(t, int64(100), result.FeeRate)
|
||||
assert.True(t, result.IsActive)
|
||||
})
|
||||
|
||||
t.Run("未授权用户创建配置应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: 5,
|
||||
MinWithdrawalAmount: 10000,
|
||||
FeeRate: 100,
|
||||
}
|
||||
|
||||
_, err := service.Create(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalSettingService_ConfigSwitch(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal_setting.New(tx, accountStore, settingStore)
|
||||
|
||||
t.Run("配置切换-旧配置自动失效", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
req1 := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: 3,
|
||||
MinWithdrawalAmount: 5000,
|
||||
FeeRate: 50,
|
||||
}
|
||||
result1, err := service.Create(ctx, req1)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result1.IsActive)
|
||||
|
||||
req2 := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: 10,
|
||||
MinWithdrawalAmount: 20000,
|
||||
FeeRate: 200,
|
||||
}
|
||||
result2, err := service.Create(ctx, req2)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result2.IsActive)
|
||||
|
||||
current, err := service.GetCurrent(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, result2.ID, current.ID)
|
||||
assert.Equal(t, 10, current.DailyWithdrawalLimit)
|
||||
assert.Equal(t, int64(20000), current.MinWithdrawalAmount)
|
||||
assert.Equal(t, int64(200), current.FeeRate)
|
||||
assert.True(t, current.IsActive)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalSettingService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal_setting.New(tx, accountStore, settingStore)
|
||||
|
||||
t.Run("查询配置列表-空结果", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
req := &dto.WithdrawalSettingListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询配置列表-有数据", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
req := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: i + 1,
|
||||
MinWithdrawalAmount: int64((i + 1) * 1000),
|
||||
FeeRate: int64((i + 1) * 10),
|
||||
}
|
||||
_, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
listReq := &dto.WithdrawalSettingListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, listReq)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(3))
|
||||
assert.NotEmpty(t, result.Items)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommissionWithdrawalSettingService_GetCurrent(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
// 清理已有的活跃配置,确保测试隔离
|
||||
tx.Exec("UPDATE tb_commission_withdrawal_setting SET is_active = false WHERE is_active = true")
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
|
||||
service := commission_withdrawal_setting.New(tx, accountStore, settingStore)
|
||||
|
||||
t.Run("获取当前配置-无配置时应返回错误", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
_, err := service.GetCurrent(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("获取当前配置-有配置时正常返回", func(t *testing.T) {
|
||||
ctx := createWithdrawalSettingTestContext(1)
|
||||
|
||||
req := &dto.CreateWithdrawalSettingReq{
|
||||
DailyWithdrawalLimit: 5,
|
||||
MinWithdrawalAmount: 10000,
|
||||
FeeRate: 100,
|
||||
}
|
||||
_, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
current, err := service.GetCurrent(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, current)
|
||||
assert.Equal(t, 5, current.DailyWithdrawalLimit)
|
||||
assert.Equal(t, int64(10000), current.MinWithdrawalAmount)
|
||||
assert.Equal(t, int64(100), current.FeeRate)
|
||||
assert.True(t, current.IsActive)
|
||||
})
|
||||
}
|
||||
@@ -1,626 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pkggorm "github.com/break/junhong_cmp_fiber/pkg/gorm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/enterprise_card"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createAgentContext(userID, shopID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypeAgent)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyShopID, shopID)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func createPlatformContext(userID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyShopID, uint(0))
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_AgentCanOnlyAuthorizeOwnCards(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shop1ID := uint(100)
|
||||
shop2ID := uint(200)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "代理1的企业",
|
||||
EnterpriseCode: "ENT_PERM_001",
|
||||
OwnerShopID: &shop1ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card1 := &model.IotCard{ICCID: "PERM_CARD_001", MSISDN: "13800001001", Status: 1, ShopID: &shop1ID}
|
||||
card2 := &model.IotCard{ICCID: "PERM_CARD_002", MSISDN: "13800001002", Status: 1, ShopID: &shop2ID}
|
||||
err = tx.Create(card1).Error
|
||||
require.NoError(t, err)
|
||||
err = tx.Create(card2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("代理可以授权自己店铺的卡", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shop1ID)
|
||||
err := authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card1.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("代理不能授权其他店铺的卡", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shop1ID)
|
||||
err := authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card2.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "不属于您的店铺")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_AgentCanOnlyAuthorizeToOwnEnterprise(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shop1ID := uint(101)
|
||||
shop2ID := uint(201)
|
||||
|
||||
ent1 := &model.Enterprise{
|
||||
EnterpriseName: "代理1的企业",
|
||||
EnterpriseCode: "ENT_PERM_101",
|
||||
OwnerShopID: &shop1ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent1.Creator = 1
|
||||
ent1.Updater = 1
|
||||
err := tx.Create(ent1).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ent2 := &model.Enterprise{
|
||||
EnterpriseName: "代理2的企业",
|
||||
EnterpriseCode: "ENT_PERM_201",
|
||||
OwnerShopID: &shop2ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent2.Creator = 2
|
||||
ent2.Updater = 2
|
||||
err = tx.Create(ent2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{ICCID: "PERM_CARD_101", MSISDN: "13800002001", Status: 1, ShopID: &shop1ID}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("代理可以授权给自己的企业", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shop1ID)
|
||||
err := authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent1.ID,
|
||||
CardIDs: []uint{card.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
card2 := &model.IotCard{ICCID: "PERM_CARD_102", MSISDN: "13800002002", Status: 1, ShopID: &shop1ID}
|
||||
err = tx.Create(card2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("代理不能授权给其他代理的企业", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shop1ID)
|
||||
err := authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent2.ID,
|
||||
CardIDs: []uint{card2.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "只能授权给自己的企业")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_PlatformCanAuthorizeAnyCard(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shop1ID := uint(301)
|
||||
shop2ID := uint(302)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "平台测试企业",
|
||||
EnterpriseCode: "ENT_PLAT_001",
|
||||
OwnerShopID: &shop1ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card1 := &model.IotCard{ICCID: "PLAT_CARD_001", MSISDN: "13800003001", Status: 1, ShopID: &shop1ID}
|
||||
card2 := &model.IotCard{ICCID: "PLAT_CARD_002", MSISDN: "13800003002", Status: 1, ShopID: &shop2ID}
|
||||
err = tx.Create(card1).Error
|
||||
require.NoError(t, err)
|
||||
err = tx.Create(card2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createPlatformContext(1)
|
||||
|
||||
t.Run("平台可以授权任意店铺的卡", func(t *testing.T) {
|
||||
err := authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card1.ID, card2.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_CannotAuthorizeBoundCard(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shopID := uint(401)
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "绑定测试企业",
|
||||
EnterpriseCode: "ENT_BOUND_001",
|
||||
OwnerShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "BOUND_CARD_001",
|
||||
MSISDN: "13800004001",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
bindTime := time.Now()
|
||||
binding := &model.DeviceSimBinding{
|
||||
DeviceID: 1,
|
||||
IotCardID: card.ID,
|
||||
SlotPosition: 1,
|
||||
BindStatus: 1,
|
||||
BindTime: &bindTime,
|
||||
}
|
||||
binding.Creator = 1
|
||||
binding.Updater = 1
|
||||
err = tx.Create(binding).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createPlatformContext(1)
|
||||
err = authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "已绑定设备")
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_CannotAuthorizeUndistributedCard(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "未分销测试企业",
|
||||
EnterpriseCode: "ENT_UNDIST_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{
|
||||
ICCID: "UNDIST_CARD_001",
|
||||
MSISDN: "13800005001",
|
||||
Status: 1,
|
||||
ShopID: nil,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createPlatformContext(1)
|
||||
err = authService.BatchAuthorize(ctx, enterprise_card.BatchAuthorizeRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card.ID},
|
||||
AuthorizerID: 1,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "未分销")
|
||||
}
|
||||
|
||||
func TestAuthorizationPermission_AgentCanOnlyRevokeOwnAuthorization(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shopID := uint(501)
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "回收测试企业",
|
||||
EnterpriseCode: "ENT_REVOKE_001",
|
||||
OwnerShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{ICCID: "REVOKE_CARD_001", MSISDN: "13800006001", Status: 1, ShopID: &shopID}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card.ID,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
}
|
||||
err = authStore.Create(context.Background(), auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("代理可以回收自己创建的授权", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shopID)
|
||||
err := authService.RevokeAuthorizations(ctx, enterprise_card.RevokeAuthorizationsRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card.ID},
|
||||
RevokedBy: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
card2 := &model.IotCard{ICCID: "REVOKE_CARD_002", MSISDN: "13800006002", Status: 1, ShopID: &shopID}
|
||||
err = tx.Create(card2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
auth2 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card2.ID,
|
||||
AuthorizedBy: 999,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypeAgent,
|
||||
}
|
||||
err = authStore.Create(context.Background(), auth2)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("代理不能回收其他人创建的授权", func(t *testing.T) {
|
||||
ctx := createAgentContext(1, shopID)
|
||||
err := authService.RevokeAuthorizations(ctx, enterprise_card.RevokeAuthorizationsRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card2.ID},
|
||||
RevokedBy: 1,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "只能回收自己创建的授权")
|
||||
})
|
||||
|
||||
t.Run("平台可以回收任何授权", func(t *testing.T) {
|
||||
ctx := createPlatformContext(2)
|
||||
err := authService.RevokeAuthorizations(ctx, enterprise_card.RevokeAuthorizationsRequest{
|
||||
EnterpriseID: ent.ID,
|
||||
CardIDs: []uint{card2.ID},
|
||||
RevokedBy: 2,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationService_ListRecords(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shopID := uint(600)
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "列表测试企业",
|
||||
EnterpriseCode: "ENT_LIST_001",
|
||||
OwnerShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card1 := &model.IotCard{ICCID: "LIST_CARD_001", MSISDN: "13800007001", Status: 1, ShopID: &shopID}
|
||||
card2 := &model.IotCard{ICCID: "LIST_CARD_002", MSISDN: "13800007002", Status: 1, ShopID: &shopID}
|
||||
err = tx.Create(card1).Error
|
||||
require.NoError(t, err)
|
||||
err = tx.Create(card2).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &model.Account{
|
||||
Username: "test_authorizer",
|
||||
Phone: "13800008001",
|
||||
Password: "hashed",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
account.Creator = 1
|
||||
account.Updater = 1
|
||||
err = tx.Create(account).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Now()
|
||||
auth1 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card1.ID,
|
||||
AuthorizedBy: account.ID,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "测试备注1",
|
||||
}
|
||||
auth2 := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card2.ID,
|
||||
AuthorizedBy: account.ID,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "测试备注2",
|
||||
}
|
||||
err = authStore.Create(context.Background(), auth1)
|
||||
require.NoError(t, err)
|
||||
err = authStore.Create(context.Background(), auth2)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := pkggorm.SkipDataPermission(context.Background())
|
||||
|
||||
t.Run("分页查询授权记录", func(t *testing.T) {
|
||||
resp, err := authService.ListRecords(ctx, enterprise_card.ListRecordsRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, resp.Total, int64(2))
|
||||
assert.GreaterOrEqual(t, len(resp.Items), 2)
|
||||
})
|
||||
|
||||
t.Run("按企业ID筛选", func(t *testing.T) {
|
||||
resp, err := authService.ListRecords(ctx, enterprise_card.ListRecordsRequest{
|
||||
EnterpriseID: &ent.ID,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), resp.Total)
|
||||
for _, item := range resp.Items {
|
||||
assert.Equal(t, ent.ID, item.EnterpriseID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("按ICCID筛选", func(t *testing.T) {
|
||||
resp, err := authService.ListRecords(ctx, enterprise_card.ListRecordsRequest{
|
||||
ICCID: "LIST_CARD_001",
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), resp.Total)
|
||||
assert.Equal(t, "LIST_CARD_001", resp.Items[0].ICCID)
|
||||
})
|
||||
|
||||
t.Run("按状态筛选-有效授权", func(t *testing.T) {
|
||||
status := 1
|
||||
resp, err := authService.ListRecords(ctx, enterprise_card.ListRecordsRequest{
|
||||
EnterpriseID: &ent.ID,
|
||||
Status: &status,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), resp.Total)
|
||||
for _, item := range resp.Items {
|
||||
assert.Equal(t, 1, item.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationService_GetRecordDetail(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shopID := uint(700)
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "详情测试企业",
|
||||
EnterpriseCode: "ENT_DETAIL_001",
|
||||
OwnerShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{ICCID: "DETAIL_CARD_001", MSISDN: "13800009001", Status: 1, ShopID: &shopID}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &model.Account{
|
||||
Username: "detail_authorizer",
|
||||
Phone: "13800009002",
|
||||
Password: "hashed",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
account.Creator = 1
|
||||
account.Updater = 1
|
||||
err = tx.Create(account).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card.ID,
|
||||
AuthorizedBy: account.ID,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "详情测试备注",
|
||||
}
|
||||
err = authStore.Create(context.Background(), auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := pkggorm.SkipDataPermission(context.Background())
|
||||
|
||||
t.Run("获取授权记录详情", func(t *testing.T) {
|
||||
detail, err := authService.GetRecordDetail(ctx, auth.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, auth.ID, detail.ID)
|
||||
assert.Equal(t, ent.ID, detail.EnterpriseID)
|
||||
assert.Equal(t, "详情测试企业", detail.EnterpriseName)
|
||||
assert.Equal(t, card.ID, detail.CardID)
|
||||
assert.Equal(t, "DETAIL_CARD_001", detail.ICCID)
|
||||
assert.Equal(t, "详情测试备注", detail.Remark)
|
||||
assert.Equal(t, 1, detail.Status)
|
||||
})
|
||||
|
||||
t.Run("获取不存在的记录", func(t *testing.T) {
|
||||
_, err := authService.GetRecordDetail(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthorizationService_UpdateRecordRemark(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||
authStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
authService := enterprise_card.NewAuthorizationService(enterpriseStore, iotCardStore, authStore, nil)
|
||||
|
||||
shopID := uint(800)
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "备注测试企业",
|
||||
EnterpriseCode: "ENT_REMARK_001",
|
||||
OwnerShopID: &shopID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
card := &model.IotCard{ICCID: "REMARK_CARD_001", MSISDN: "13800010001", Status: 1, ShopID: &shopID}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &model.Account{
|
||||
Username: "remark_authorizer",
|
||||
Phone: "13800010002",
|
||||
Password: "hashed",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
account.Creator = 1
|
||||
account.Updater = 1
|
||||
err = tx.Create(account).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: ent.ID,
|
||||
CardID: card.ID,
|
||||
AuthorizedBy: account.ID,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "原始备注",
|
||||
}
|
||||
err = authStore.Create(context.Background(), auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := pkggorm.SkipDataPermission(context.Background())
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, account.ID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
|
||||
t.Run("更新授权备注", func(t *testing.T) {
|
||||
updated, err := authService.UpdateRecordRemark(ctx, auth.ID, "更新后的备注")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "更新后的备注", updated.Remark)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的记录", func(t *testing.T) {
|
||||
_, err := authService.UpdateRecordRemark(ctx, 99999, "不会更新")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 1,
|
||||
CardID: 100,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, auth.ID)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_BatchCreate(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
auths := []*model.EnterpriseCardAuthorization{
|
||||
{
|
||||
EnterpriseID: 1,
|
||||
CardID: 101,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
},
|
||||
{
|
||||
EnterpriseID: 1,
|
||||
CardID: 102,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
},
|
||||
}
|
||||
|
||||
err := store.BatchCreate(ctx, auths)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, auth := range auths {
|
||||
assert.NotZero(t, auth.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_GetByEnterpriseAndCard(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 2,
|
||||
CardID: 200,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByEnterpriseAndCard(ctx, 2, 200)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, auth.ID, found.ID)
|
||||
assert.Equal(t, uint(2), found.EnterpriseID)
|
||||
assert.Equal(t, uint(200), found.CardID)
|
||||
|
||||
_, err = store.GetByEnterpriseAndCard(ctx, 999, 999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_ListByEnterprise(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
revokedAt := time.Now()
|
||||
|
||||
auths := []*model.EnterpriseCardAuthorization{
|
||||
{EnterpriseID: 3, CardID: 301, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 3, CardID: 302, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 3, CardID: 303, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform, RevokedAt: &revokedAt, RevokedBy: ptrUint(1)},
|
||||
}
|
||||
err := store.BatchCreate(ctx, auths)
|
||||
require.NoError(t, err)
|
||||
|
||||
activeAuths, err := store.ListByEnterprise(ctx, 3, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, activeAuths, 2)
|
||||
|
||||
allAuths, err := store.ListByEnterprise(ctx, 3, true)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, allAuths, 3)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_RevokeAuthorizations(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
auths := []*model.EnterpriseCardAuthorization{
|
||||
{EnterpriseID: 4, CardID: 401, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 4, CardID: 402, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
}
|
||||
err := store.BatchCreate(ctx, auths)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.RevokeAuthorizations(ctx, 4, []uint{401}, 2)
|
||||
require.NoError(t, err)
|
||||
|
||||
activeAuths, err := store.ListByEnterprise(ctx, 4, false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, activeAuths, 1)
|
||||
assert.Equal(t, uint(402), activeAuths[0].CardID)
|
||||
|
||||
allAuths, err := store.ListByEnterprise(ctx, 4, true)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, allAuths, 2)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_GetActiveAuthorizedCardIDs(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
revokedAt := time.Now()
|
||||
auths := []*model.EnterpriseCardAuthorization{
|
||||
{EnterpriseID: 5, CardID: 501, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 5, CardID: 502, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 5, CardID: 503, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform, RevokedAt: &revokedAt, RevokedBy: ptrUint(1)},
|
||||
}
|
||||
err := store.BatchCreate(ctx, auths)
|
||||
require.NoError(t, err)
|
||||
|
||||
cardIDs, err := store.GetActiveAuthorizedCardIDs(ctx, 5)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, cardIDs, 2)
|
||||
assert.Contains(t, cardIDs, uint(501))
|
||||
assert.Contains(t, cardIDs, uint(502))
|
||||
assert.NotContains(t, cardIDs, uint(503))
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_CheckAuthorizationExists(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 6,
|
||||
CardID: 600,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
exists, err := store.CheckAuthorizationExists(ctx, 6, 600)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
exists, err = store.CheckAuthorizationExists(ctx, 6, 999)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_GetActiveAuthsByCardIDs(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
auths := []*model.EnterpriseCardAuthorization{
|
||||
{EnterpriseID: 7, CardID: 701, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
{EnterpriseID: 7, CardID: 702, AuthorizedBy: 1, AuthorizedAt: now, AuthorizerType: constants.UserTypePlatform},
|
||||
}
|
||||
err := store.BatchCreate(ctx, auths)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := store.GetActiveAuthsByCardIDs(ctx, 7, []uint{701, 702, 703})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result[701])
|
||||
assert.True(t, result[702])
|
||||
assert.False(t, result[703])
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_ListWithOptions(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
for i := uint(0); i < 15; i++ {
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 8,
|
||||
CardID: 800 + i,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
enterpriseID := uint(8)
|
||||
opts := postgres.AuthorizationListOptions{
|
||||
EnterpriseID: &enterpriseID,
|
||||
Limit: 10,
|
||||
Offset: 0,
|
||||
}
|
||||
auths, total, err := store.ListWithOptions(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(15), total)
|
||||
assert.Len(t, auths, 10)
|
||||
|
||||
opts.Offset = 10
|
||||
auths, total, err = store.ListWithOptions(ctx, opts)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(15), total)
|
||||
assert.Len(t, auths, 5)
|
||||
}
|
||||
|
||||
func ptrUint(v uint) *uint {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_UpdateRemark(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 10,
|
||||
CardID: 1000,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
Remark: "原始备注",
|
||||
}
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.UpdateRemark(ctx, auth.ID, "更新后的备注")
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := store.GetByID(ctx, auth.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "更新后的备注", updated.Remark)
|
||||
|
||||
err = store.UpdateRemark(ctx, 99999, "不存在的记录")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEnterpriseCardAuthorizationStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
auth := &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: 11,
|
||||
CardID: 1100,
|
||||
AuthorizedBy: 1,
|
||||
AuthorizedAt: time.Now(),
|
||||
AuthorizerType: constants.UserTypePlatform,
|
||||
}
|
||||
err := store.Create(ctx, auth)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByID(ctx, auth.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, auth.ID, found.ID)
|
||||
assert.Equal(t, uint(11), found.EnterpriseID)
|
||||
assert.Equal(t, uint(1100), found.CardID)
|
||||
|
||||
_, err = store.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -1,540 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/enterprise_card"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createEnterpriseCardTestContext(userID uint, shopID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyShopID, shopID)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestEnterpriseCardService_AllocateCardsPreview(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := enterprise_card.New(tx, enterpriseStore, enterpriseCardAuthStore)
|
||||
|
||||
t.Run("授权预检-企业不存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
req := &dto.AllocateCardsPreviewReq{
|
||||
ICCIDs: []string{"898600000001"},
|
||||
}
|
||||
|
||||
_, err := service.AllocateCardsPreview(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("授权预检-未授权用户应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := &dto.AllocateCardsPreviewReq{
|
||||
ICCIDs: []string{"898600000001"},
|
||||
}
|
||||
|
||||
_, err := service.AllocateCardsPreview(ctx, 1, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("授权预检-空ICCID列表", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "预检测试企业",
|
||||
EnterpriseCode: "ENT_PREVIEW_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.AllocateCardsPreviewReq{
|
||||
ICCIDs: []string{},
|
||||
}
|
||||
|
||||
result, err := service.AllocateCardsPreview(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 0, result.Summary.TotalCardCount)
|
||||
})
|
||||
|
||||
t.Run("授权预检-卡不存在", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "预检测试企业2",
|
||||
EnterpriseCode: "ENT_PREVIEW_002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.AllocateCardsPreviewReq{
|
||||
ICCIDs: []string{"NON_EXIST_ICCID"},
|
||||
}
|
||||
|
||||
result, err := service.AllocateCardsPreview(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 1, result.Summary.FailedCount)
|
||||
assert.Len(t, result.FailedItems, 1)
|
||||
assert.Equal(t, "卡不存在", result.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("授权预检-独立卡", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "预检测试企业3",
|
||||
EnterpriseCode: "ENT_PREVIEW_003",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600001234567890",
|
||||
MSISDN: "13800000001",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.AllocateCardsPreviewReq{
|
||||
ICCIDs: []string{"898600001234567890"},
|
||||
}
|
||||
|
||||
result, err := service.AllocateCardsPreview(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 1, result.Summary.StandaloneCardCount)
|
||||
assert.Len(t, result.StandaloneCards, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseCardService_AllocateCards(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := enterprise_card.New(tx, enterpriseStore, enterpriseCardAuthStore)
|
||||
|
||||
t.Run("授权卡-企业不存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
req := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600000001"},
|
||||
}
|
||||
|
||||
_, err := service.AllocateCards(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("授权卡-成功", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "授权测试企业",
|
||||
EnterpriseCode: "ENT_ALLOC_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600002345678901",
|
||||
MSISDN: "13800000002",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600002345678901"},
|
||||
}
|
||||
|
||||
result, err := service.AllocateCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, 1, result.SuccessCount)
|
||||
})
|
||||
|
||||
t.Run("授权卡-重复授权不创建新记录", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "重复授权测试企业",
|
||||
EnterpriseCode: "ENT_ALLOC_002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600003456789012",
|
||||
MSISDN: "13800000003",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600003456789012"},
|
||||
}
|
||||
|
||||
_, err = service.AllocateCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = service.AllocateCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int64
|
||||
tx.Model(&model.EnterpriseCardAuthorization{}).
|
||||
Where("enterprise_id = ? AND card_id = ?", ent.ID, card.ID).
|
||||
Count(&count)
|
||||
assert.Equal(t, int64(1), count)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseCardService_RecallCards(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := enterprise_card.New(tx, enterpriseStore, enterpriseCardAuthStore)
|
||||
|
||||
t.Run("回收授权-企业不存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
req := &dto.RecallCardsReq{
|
||||
ICCIDs: []string{"898600000001"},
|
||||
}
|
||||
|
||||
_, err := service.RecallCards(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("回收授权-卡未授权应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "回收测试企业",
|
||||
EnterpriseCode: "ENT_RECALL_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600004567890123",
|
||||
MSISDN: "13800000004",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.RecallCardsReq{
|
||||
ICCIDs: []string{"898600004567890123"},
|
||||
}
|
||||
|
||||
result, err := service.RecallCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.FailCount)
|
||||
assert.Equal(t, "该卡未授权给此企业", result.FailedItems[0].Reason)
|
||||
})
|
||||
|
||||
t.Run("回收授权-成功", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "回收成功测试企业",
|
||||
EnterpriseCode: "ENT_RECALL_002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600005678901234",
|
||||
MSISDN: "13800000005",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
allocReq := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600005678901234"},
|
||||
}
|
||||
_, err = service.AllocateCards(ctx, ent.ID, allocReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
recallReq := &dto.RecallCardsReq{
|
||||
ICCIDs: []string{"898600005678901234"},
|
||||
}
|
||||
result, err := service.RecallCards(ctx, ent.ID, recallReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.SuccessCount)
|
||||
assert.Equal(t, 0, result.FailCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseCardService_ListCards(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := enterprise_card.New(tx, enterpriseStore, enterpriseCardAuthStore)
|
||||
|
||||
t.Run("查询企业卡列表-企业不存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
req := &dto.EnterpriseCardListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
_, err := service.ListCards(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("查询企业卡列表-空结果", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "列表测试企业",
|
||||
EnterpriseCode: "ENT_LIST_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.EnterpriseCardListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, int64(0), result.Total)
|
||||
})
|
||||
|
||||
t.Run("查询企业卡列表-有数据", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "列表数据测试企业",
|
||||
EnterpriseCode: "ENT_LIST_002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600006789012345",
|
||||
MSISDN: "13800000006",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
allocReq := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600006789012345"},
|
||||
}
|
||||
_, err = service.AllocateCards(ctx, ent.ID, allocReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.EnterpriseCardListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, int64(1), result.Total)
|
||||
assert.Len(t, result.Items, 1)
|
||||
assert.Equal(t, "898600006789012345", result.Items[0].ICCID)
|
||||
})
|
||||
|
||||
t.Run("查询企业卡列表-按ICCID筛选", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "筛选测试企业",
|
||||
EnterpriseCode: "ENT_LIST_003",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600007890123456",
|
||||
MSISDN: "13800000007",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
allocReq := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600007890123456"},
|
||||
}
|
||||
_, err = service.AllocateCards(ctx, ent.ID, allocReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.EnterpriseCardListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
ICCID: "78901",
|
||||
}
|
||||
|
||||
result, err := service.ListCards(ctx, ent.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(1))
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseCardService_SuspendAndResumeCard(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
enterpriseCardAuthStore := postgres.NewEnterpriseCardAuthorizationStore(tx, rdb)
|
||||
|
||||
service := enterprise_card.New(tx, enterpriseStore, enterpriseCardAuthStore)
|
||||
|
||||
t.Run("停机-未授权的卡应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "停机测试企业",
|
||||
EnterpriseCode: "ENT_SUSPEND_001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600008901234567",
|
||||
MSISDN: "13800000008",
|
||||
Status: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.SuspendCard(ctx, ent.ID, card.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("停机和复机-成功", func(t *testing.T) {
|
||||
ctx := createEnterpriseCardTestContext(1, 1)
|
||||
|
||||
ent := &model.Enterprise{
|
||||
EnterpriseName: "停复机测试企业",
|
||||
EnterpriseCode: "ENT_SUSPEND_002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
ent.Creator = 1
|
||||
ent.Updater = 1
|
||||
err := tx.Create(ent).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
shopID := uint(1)
|
||||
card := &model.IotCard{
|
||||
ICCID: "898600009012345678",
|
||||
MSISDN: "13800000009",
|
||||
Status: 1,
|
||||
NetworkStatus: 1,
|
||||
ShopID: &shopID,
|
||||
}
|
||||
err = tx.Create(card).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
allocReq := &dto.AllocateCardsReq{
|
||||
ICCIDs: []string{"898600009012345678"},
|
||||
}
|
||||
_, err = service.AllocateCards(ctx, ent.ID, allocReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.SuspendCard(ctx, ent.ID, card.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var suspendedCard model.IotCard
|
||||
tx.First(&suspendedCard, card.ID)
|
||||
assert.Equal(t, 0, suspendedCard.NetworkStatus)
|
||||
|
||||
err = service.ResumeCard(ctx, ent.ID, card.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
var resumedCard model.IotCard
|
||||
tx.First(&resumedCard, card.ID)
|
||||
assert.Equal(t, 1, resumedCard.NetworkStatus)
|
||||
})
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/enterprise"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createEnterpriseTestContext(userID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestEnterpriseService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
|
||||
service := enterprise.New(tx, enterpriseStore, shopStore, accountStore)
|
||||
|
||||
t.Run("创建企业-含账号创建", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
req := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "测试企业",
|
||||
EnterpriseCode: "ENT_TEST_001",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
LoginPhone: "13900000001",
|
||||
Password: "Test123456",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "测试企业", result.Enterprise.EnterpriseName)
|
||||
assert.Equal(t, "ENT_TEST_001", result.Enterprise.EnterpriseCode)
|
||||
assert.Equal(t, constants.StatusEnabled, result.Enterprise.Status)
|
||||
assert.Greater(t, result.AccountID, uint(0))
|
||||
})
|
||||
|
||||
t.Run("创建企业-企业编号已存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
req1 := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "企业一",
|
||||
EnterpriseCode: "ENT_DUP_001",
|
||||
ContactName: "联系人一",
|
||||
ContactPhone: "13800000010",
|
||||
LoginPhone: "13900000010",
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err := service.Create(ctx, req1)
|
||||
require.NoError(t, err)
|
||||
|
||||
req2 := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "企业二",
|
||||
EnterpriseCode: "ENT_DUP_001",
|
||||
ContactName: "联系人二",
|
||||
ContactPhone: "13800000011",
|
||||
LoginPhone: "13900000011",
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err = service.Create(ctx, req2)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("创建企业-手机号已存在应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
req1 := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "企业三",
|
||||
EnterpriseCode: "ENT_PHONE_001",
|
||||
ContactName: "联系人三",
|
||||
ContactPhone: "13800000020",
|
||||
LoginPhone: "13900000020",
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err := service.Create(ctx, req1)
|
||||
require.NoError(t, err)
|
||||
|
||||
req2 := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "企业四",
|
||||
EnterpriseCode: "ENT_PHONE_002",
|
||||
ContactName: "联系人四",
|
||||
ContactPhone: "13800000021",
|
||||
LoginPhone: "13900000020",
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err = service.Create(ctx, req2)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("创建企业-未授权用户应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "未授权企业",
|
||||
EnterpriseCode: "ENT_UNAUTH_001",
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000030",
|
||||
LoginPhone: "13900000030",
|
||||
Password: "Test123456",
|
||||
}
|
||||
|
||||
_, err := service.Create(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
|
||||
service := enterprise.New(tx, enterpriseStore, shopStore, accountStore)
|
||||
|
||||
t.Run("编辑企业", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "待编辑企业",
|
||||
EnterpriseCode: "ENT_EDIT_001",
|
||||
ContactName: "原联系人",
|
||||
ContactPhone: "13800000040",
|
||||
LoginPhone: "13900000040",
|
||||
Password: "Test123456",
|
||||
}
|
||||
createResult, err := service.Create(ctx, createReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
newName := "编辑后企业"
|
||||
newContact := "新联系人"
|
||||
updateReq := &dto.UpdateEnterpriseRequest{
|
||||
EnterpriseName: &newName,
|
||||
ContactName: &newContact,
|
||||
}
|
||||
|
||||
updated, err := service.Update(ctx, createResult.Enterprise.ID, updateReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "编辑后企业", updated.EnterpriseName)
|
||||
assert.Equal(t, "新联系人", updated.ContactName)
|
||||
})
|
||||
|
||||
t.Run("编辑不存在的企业应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
newName := "不存在企业"
|
||||
updateReq := &dto.UpdateEnterpriseRequest{
|
||||
EnterpriseName: &newName,
|
||||
}
|
||||
|
||||
_, err := service.Update(ctx, 99999, updateReq)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
|
||||
service := enterprise.New(tx, enterpriseStore, shopStore, accountStore)
|
||||
|
||||
t.Run("禁用企业-账号同步禁用", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "待禁用企业",
|
||||
EnterpriseCode: "ENT_STATUS_001",
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000050",
|
||||
LoginPhone: "13900000050",
|
||||
Password: "Test123456",
|
||||
}
|
||||
createResult, err := service.Create(ctx, createReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.UpdateStatus(ctx, createResult.Enterprise.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
ent, err := service.GetByID(ctx, createResult.Enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, ent.Status)
|
||||
|
||||
var account model.Account
|
||||
err = tx.Where("enterprise_id = ?", createResult.Enterprise.ID).First(&account).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, account.Status)
|
||||
})
|
||||
|
||||
t.Run("启用企业-账号同步启用", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "待启用企业",
|
||||
EnterpriseCode: "ENT_STATUS_002",
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000051",
|
||||
LoginPhone: "13900000051",
|
||||
Password: "Test123456",
|
||||
}
|
||||
createResult, err := service.Create(ctx, createReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.UpdateStatus(ctx, createResult.Enterprise.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.UpdateStatus(ctx, createResult.Enterprise.ID, constants.StatusEnabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
ent, err := service.GetByID(ctx, createResult.Enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, ent.Status)
|
||||
|
||||
var account model.Account
|
||||
err = tx.Where("enterprise_id = ?", createResult.Enterprise.ID).First(&account).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, account.Status)
|
||||
})
|
||||
|
||||
t.Run("更新不存在企业状态应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
err := service.UpdateStatus(ctx, 99999, constants.StatusDisabled)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseService_UpdatePassword(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
|
||||
service := enterprise.New(tx, enterpriseStore, shopStore, accountStore)
|
||||
|
||||
t.Run("修改企业账号密码", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: "密码测试企业",
|
||||
EnterpriseCode: "ENT_PWD_001",
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000060",
|
||||
LoginPhone: "13900000060",
|
||||
Password: "OldPass123",
|
||||
}
|
||||
createResult, err := service.Create(ctx, createReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.UpdatePassword(ctx, createResult.Enterprise.ID, "NewPass456")
|
||||
require.NoError(t, err)
|
||||
|
||||
var account model.Account
|
||||
err = tx.Where("enterprise_id = ?", createResult.Enterprise.ID).First(&account).Error
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, "OldPass123", account.Password)
|
||||
assert.NotEqual(t, "NewPass456", account.Password)
|
||||
})
|
||||
|
||||
t.Run("修改不存在企业密码应失败", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
err := service.UpdatePassword(ctx, 99999, "NewPass789")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnterpriseService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
|
||||
service := enterprise.New(tx, enterpriseStore, shopStore, accountStore)
|
||||
|
||||
t.Run("查询企业列表-空结果", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
req := &dto.EnterpriseListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询企业列表-按名称筛选", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
searchKey := fmt.Sprintf("列表测试企业_%d", ts)
|
||||
for i := 0; i < 3; i++ {
|
||||
createReq := &dto.CreateEnterpriseReq{
|
||||
EnterpriseName: fmt.Sprintf("%s_%d", searchKey, i),
|
||||
EnterpriseCode: fmt.Sprintf("ENT_LIST_%d_%d", ts, i),
|
||||
ContactName: "联系人",
|
||||
ContactPhone: fmt.Sprintf("138%08d", ts%100000000+int64(i)),
|
||||
LoginPhone: fmt.Sprintf("139%08d", ts%100000000+int64(i)),
|
||||
Password: "Test123456",
|
||||
}
|
||||
_, err := service.Create(ctx, createReq)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
req := &dto.EnterpriseListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
EnterpriseName: searchKey,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(3))
|
||||
})
|
||||
|
||||
t.Run("查询企业列表-按状态筛选", func(t *testing.T) {
|
||||
ctx := createEnterpriseTestContext(1)
|
||||
|
||||
status := constants.StatusEnabled
|
||||
req := &dto.EnterpriseListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
Status: &status,
|
||||
}
|
||||
|
||||
result, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
@@ -1,415 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestEnterpriseStore_Create 测试创建企业
|
||||
func TestEnterpriseStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
enterprise *model.Enterprise
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "创建平台直属企业",
|
||||
enterprise: &model.Enterprise{
|
||||
EnterpriseName: "测试企业A",
|
||||
EnterpriseCode: "ENT001",
|
||||
OwnerShopID: nil, // 平台直属
|
||||
LegalPerson: "张三",
|
||||
ContactName: "李四",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Province: "北京市",
|
||||
City: "北京市",
|
||||
District: "朝阳区",
|
||||
Address: "朝阳路100号",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "创建归属店铺的企业",
|
||||
enterprise: &model.Enterprise{
|
||||
EnterpriseName: "测试企业B",
|
||||
EnterpriseCode: "ENT002",
|
||||
LegalPerson: "王五",
|
||||
ContactName: "赵六",
|
||||
ContactPhone: "13800000002",
|
||||
BusinessLicense: "91110000MA005678",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.enterprise.BaseModel.Creator = 1
|
||||
tt.enterprise.BaseModel.Updater = 1
|
||||
|
||||
err := store.Create(ctx, tt.enterprise)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, tt.enterprise.ID)
|
||||
assert.NotZero(t, tt.enterprise.CreatedAt)
|
||||
assert.NotZero(t, tt.enterprise.UpdatedAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_GetByID 测试根据 ID 查询企业
|
||||
func TestEnterpriseStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试企业
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: "测试企业",
|
||||
EnterpriseCode: "TEST001",
|
||||
LegalPerson: "测试法人",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询存在的企业", func(t *testing.T) {
|
||||
found, err := store.GetByID(ctx, enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, enterprise.EnterpriseName, found.EnterpriseName)
|
||||
assert.Equal(t, enterprise.EnterpriseCode, found.EnterpriseCode)
|
||||
assert.Equal(t, enterprise.LegalPerson, found.LegalPerson)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的企业", func(t *testing.T) {
|
||||
_, err := store.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_GetByCode 测试根据企业编号查询
|
||||
func TestEnterpriseStore_GetByCode(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试企业
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: "测试企业",
|
||||
EnterpriseCode: "UNIQUE001",
|
||||
LegalPerson: "测试法人",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据企业编号查询", func(t *testing.T) {
|
||||
found, err := store.GetByCode(ctx, "UNIQUE001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, enterprise.ID, found.ID)
|
||||
assert.Equal(t, enterprise.EnterpriseName, found.EnterpriseName)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的企业编号", func(t *testing.T) {
|
||||
_, err := store.GetByCode(ctx, "NONEXISTENT")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_Update 测试更新企业
|
||||
func TestEnterpriseStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试企业
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: "原始企业名称",
|
||||
EnterpriseCode: "UPDATE001",
|
||||
LegalPerson: "原法人",
|
||||
ContactName: "原联系人",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新企业信息", func(t *testing.T) {
|
||||
enterprise.EnterpriseName = "更新后的企业名称"
|
||||
enterprise.LegalPerson = "新法人"
|
||||
enterprise.ContactName = "新联系人"
|
||||
enterprise.ContactPhone = "13900000001"
|
||||
enterprise.Updater = 2
|
||||
|
||||
err := store.Update(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.GetByID(ctx, enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "更新后的企业名称", found.EnterpriseName)
|
||||
assert.Equal(t, "新法人", found.LegalPerson)
|
||||
assert.Equal(t, "新联系人", found.ContactName)
|
||||
assert.Equal(t, "13900000001", found.ContactPhone)
|
||||
assert.Equal(t, uint(2), found.Updater)
|
||||
})
|
||||
|
||||
t.Run("更新企业状态", func(t *testing.T) {
|
||||
enterprise.Status = constants.StatusDisabled
|
||||
err := store.Update(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByID(ctx, enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, found.Status)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_Delete 测试软删除企业
|
||||
func TestEnterpriseStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试企业
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: "待删除企业",
|
||||
EnterpriseCode: "DELETE001",
|
||||
LegalPerson: "测试",
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除企业", func(t *testing.T) {
|
||||
err := store.Delete(ctx, enterprise.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证已被软删除
|
||||
_, err = store.GetByID(ctx, enterprise.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_List 测试查询企业列表
|
||||
func TestEnterpriseStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建多个测试企业
|
||||
for i := 1; i <= 5; i++ {
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: testutils.GenerateUsername("测试企业", i),
|
||||
EnterpriseCode: testutils.GenerateUsername("ENT", i),
|
||||
LegalPerson: "测试法人",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: testutils.GeneratePhone("138", i),
|
||||
BusinessLicense: testutils.GenerateUsername("LICENSE", i),
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
enterprises, total, err := store.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(enterprises), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
|
||||
t.Run("带过滤条件查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"status": constants.StatusEnabled,
|
||||
}
|
||||
enterprises, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
for _, ent := range enterprises {
|
||||
assert.Equal(t, constants.StatusEnabled, ent.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_GetByOwnerShopID 测试根据归属店铺查询企业
|
||||
func TestEnterpriseStore_GetByOwnerShopID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
shopID1 := uint(100)
|
||||
shopID2 := uint(200)
|
||||
|
||||
// 创建归属不同店铺的企业
|
||||
for i := 1; i <= 3; i++ {
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: testutils.GenerateUsername("店铺100企业", i),
|
||||
EnterpriseCode: testutils.GenerateUsername("SHOP100_ENT", i),
|
||||
OwnerShopID: &shopID1,
|
||||
LegalPerson: "测试法人",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: testutils.GeneratePhone("138", i),
|
||||
BusinessLicense: testutils.GenerateUsername("LICENSE", i),
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for i := 1; i <= 2; i++ {
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: testutils.GenerateUsername("店铺200企业", i),
|
||||
EnterpriseCode: testutils.GenerateUsername("SHOP200_ENT", i),
|
||||
OwnerShopID: &shopID2,
|
||||
LegalPerson: "测试法人",
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: testutils.GeneratePhone("139", i),
|
||||
BusinessLicense: testutils.GenerateUsername("LICENSE2", i),
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("查询店铺100的企业", func(t *testing.T) {
|
||||
enterprises, err := store.GetByOwnerShopID(ctx, shopID1)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, enterprises, 3)
|
||||
for _, ent := range enterprises {
|
||||
assert.NotNil(t, ent.OwnerShopID)
|
||||
assert.Equal(t, shopID1, *ent.OwnerShopID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("查询店铺200的企业", func(t *testing.T) {
|
||||
enterprises, err := store.GetByOwnerShopID(ctx, shopID2)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, enterprises, 2)
|
||||
for _, ent := range enterprises {
|
||||
assert.NotNil(t, ent.OwnerShopID)
|
||||
assert.Equal(t, shopID2, *ent.OwnerShopID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestEnterpriseStore_UniqueConstraints 测试唯一约束
|
||||
func TestEnterpriseStore_UniqueConstraints(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewEnterpriseStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试企业
|
||||
enterprise := &model.Enterprise{
|
||||
EnterpriseName: "唯一测试企业",
|
||||
EnterpriseCode: "UNIQUE_CODE",
|
||||
LegalPerson: "测试",
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000001",
|
||||
BusinessLicense: "91110000MA001234",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, enterprise)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("重复企业编号应失败", func(t *testing.T) {
|
||||
duplicate := &model.Enterprise{
|
||||
EnterpriseName: "另一个企业",
|
||||
EnterpriseCode: "UNIQUE_CODE", // 重复
|
||||
LegalPerson: "测试",
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000002",
|
||||
BusinessLicense: "91110000MA005678",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, duplicate)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// createContextWithUserID 创建带用户 ID 的 context
|
||||
func createContextWithUserID(userID uint) context.Context {
|
||||
return context.WithValue(context.Background(), constants.ContextKeyUserID, userID)
|
||||
}
|
||||
|
||||
// generateUniqueUsername 生成唯一的用户名(用于测试)
|
||||
func generateUniqueUsername(prefix string, t *testing.T) string {
|
||||
return fmt.Sprintf("%s_%d", prefix, time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// generateUniquePhone 生成唯一的手机号(用于测试)
|
||||
func generateUniquePhone() string {
|
||||
// 使用时间戳后8位生成唯一手机号
|
||||
timestamp := time.Now().UnixNano()
|
||||
suffix := timestamp % 100000000 // 8位数字
|
||||
return fmt.Sprintf("138%08d", suffix)
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/my_commission"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createMyCommissionTestContext(userID uint, shopID uint, userType int) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, userType)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyShopID, shopID)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestMyCommissionService_GetCommissionSummary(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionWithdrawalSettingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
|
||||
service := my_commission.New(
|
||||
tx, shopStore, walletStore,
|
||||
commissionWithdrawalRequestStore, commissionWithdrawalSettingStore,
|
||||
commissionRecordStore, walletTransactionStore,
|
||||
)
|
||||
|
||||
t.Run("佣金概览-代理商用户成功", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "概览测试店铺",
|
||||
ShopCode: "MY_SHOP_001",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
result, err := service.GetCommissionSummary(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, shop.ID, result.ShopID)
|
||||
assert.Equal(t, "概览测试店铺", result.ShopName)
|
||||
})
|
||||
|
||||
t.Run("佣金概览-非代理商用户应失败", func(t *testing.T) {
|
||||
ctx := createMyCommissionTestContext(1, 1, constants.UserTypePlatform)
|
||||
|
||||
_, err := service.GetCommissionSummary(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("佣金概览-店铺不存在应失败", func(t *testing.T) {
|
||||
ctx := createMyCommissionTestContext(1, 99999, constants.UserTypeAgent)
|
||||
|
||||
_, err := service.GetCommissionSummary(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMyCommissionService_CreateWithdrawalRequest(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionWithdrawalSettingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
|
||||
service := my_commission.New(
|
||||
tx, shopStore, walletStore,
|
||||
commissionWithdrawalRequestStore, commissionWithdrawalSettingStore,
|
||||
commissionRecordStore, walletTransactionStore,
|
||||
)
|
||||
|
||||
t.Run("发起提现-无提现配置应失败", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "提现测试店铺",
|
||||
ShopCode: "MY_SHOP_002",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
req := &dto.CreateMyWithdrawalReq{
|
||||
Amount: 10000,
|
||||
WithdrawalMethod: "alipay",
|
||||
AccountName: "测试用户",
|
||||
AccountNumber: "test@alipay.com",
|
||||
}
|
||||
|
||||
_, err = service.CreateWithdrawalRequest(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("发起提现-金额低于最低限制应失败", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "限额测试店铺",
|
||||
ShopCode: "MY_SHOP_003",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000003",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
setting := &model.CommissionWithdrawalSetting{
|
||||
DailyWithdrawalLimit: 5,
|
||||
MinWithdrawalAmount: 10000,
|
||||
FeeRate: 100,
|
||||
IsActive: true,
|
||||
}
|
||||
setting.Creator = 1
|
||||
setting.Updater = 1
|
||||
err = tx.Create(setting).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
req := &dto.CreateMyWithdrawalReq{
|
||||
Amount: 5000,
|
||||
WithdrawalMethod: "alipay",
|
||||
AccountName: "测试用户",
|
||||
AccountNumber: "test@alipay.com",
|
||||
}
|
||||
|
||||
_, err = service.CreateWithdrawalRequest(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("发起提现-余额不足应失败", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "余额测试店铺",
|
||||
ShopCode: "MY_SHOP_004",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000004",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
wallet := &model.Wallet{
|
||||
ResourceType: constants.WalletResourceTypeShop,
|
||||
ResourceID: shop.ID,
|
||||
WalletType: constants.WalletTypeCommission,
|
||||
Balance: 5000,
|
||||
}
|
||||
err = tx.Create(wallet).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
req := &dto.CreateMyWithdrawalReq{
|
||||
Amount: 50000,
|
||||
WithdrawalMethod: "alipay",
|
||||
AccountName: "测试用户",
|
||||
AccountNumber: "test@alipay.com",
|
||||
}
|
||||
|
||||
_, err = service.CreateWithdrawalRequest(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("发起提现-非代理商用户应失败", func(t *testing.T) {
|
||||
ctx := createMyCommissionTestContext(1, 1, constants.UserTypePlatform)
|
||||
|
||||
req := &dto.CreateMyWithdrawalReq{
|
||||
Amount: 10000,
|
||||
WithdrawalMethod: "alipay",
|
||||
AccountName: "测试用户",
|
||||
AccountNumber: "test@alipay.com",
|
||||
}
|
||||
|
||||
_, err := service.CreateWithdrawalRequest(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMyCommissionService_ListMyWithdrawalRequests(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionWithdrawalSettingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
|
||||
service := my_commission.New(
|
||||
tx, shopStore, walletStore,
|
||||
commissionWithdrawalRequestStore, commissionWithdrawalSettingStore,
|
||||
commissionRecordStore, walletTransactionStore,
|
||||
)
|
||||
|
||||
t.Run("查询提现记录-空结果", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "提现记录测试店铺",
|
||||
ShopCode: "MY_SHOP_005",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000005",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
req := &dto.MyWithdrawalListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListMyWithdrawalRequests(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询提现记录-按状态筛选", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "状态筛选测试店铺",
|
||||
ShopCode: "MY_SHOP_006",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000006",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
status := 1
|
||||
req := &dto.MyWithdrawalListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
Status: &status,
|
||||
}
|
||||
|
||||
result, err := service.ListMyWithdrawalRequests(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("查询提现记录-非代理商用户应失败", func(t *testing.T) {
|
||||
ctx := createMyCommissionTestContext(1, 1, constants.UserTypePlatform)
|
||||
|
||||
req := &dto.MyWithdrawalListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
_, err := service.ListMyWithdrawalRequests(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMyCommissionService_ListMyCommissionRecords(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionWithdrawalSettingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||
|
||||
service := my_commission.New(
|
||||
tx, shopStore, walletStore,
|
||||
commissionWithdrawalRequestStore, commissionWithdrawalSettingStore,
|
||||
commissionRecordStore, walletTransactionStore,
|
||||
)
|
||||
|
||||
t.Run("查询佣金明细-空结果", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "佣金明细测试店铺",
|
||||
ShopCode: "MY_SHOP_007",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000007",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
req := &dto.MyCommissionRecordListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListMyCommissionRecords(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询佣金明细-按类型筛选", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "类型筛选测试店铺",
|
||||
ShopCode: "MY_SHOP_008",
|
||||
Level: 1,
|
||||
ContactName: "联系人",
|
||||
ContactPhone: "13800000008",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
shop.Creator = 1
|
||||
shop.Updater = 1
|
||||
err := tx.Create(shop).Error
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||
|
||||
commissionSource := "one_time"
|
||||
req := &dto.MyCommissionRecordListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
CommissionSource: &commissionSource,
|
||||
}
|
||||
|
||||
result, err := service.ListMyCommissionRecords(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
|
||||
t.Run("查询佣金明细-非代理商用户应失败", func(t *testing.T) {
|
||||
ctx := createMyCommissionTestContext(1, 1, constants.UserTypePlatform)
|
||||
|
||||
req := &dto.MyCommissionRecordListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
_, err := service.ListMyCommissionRecords(ctx, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/permission"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPermissionCache_FirstCallMissSecondHit(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
|
||||
permSvc := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
testUser := &model.Account{
|
||||
Username: "testuser",
|
||||
Phone: "13900000001",
|
||||
Password: "Test@123456",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, accountStore.Create(ctx, testUser))
|
||||
|
||||
testRole := &model.Role{
|
||||
RoleName: "测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, roleStore.Create(ctx, testRole))
|
||||
|
||||
testPerm := &model.Permission{
|
||||
PermName: "测试权限",
|
||||
PermCode: "test:read",
|
||||
PermType: constants.PermissionTypeButton,
|
||||
Platform: constants.PlatformWeb,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, permStore.Create(ctx, testPerm))
|
||||
|
||||
require.NoError(t, accountRoleStore.Create(ctx, &model.AccountRole{
|
||||
AccountID: testUser.ID,
|
||||
RoleID: testRole.ID,
|
||||
}))
|
||||
|
||||
require.NoError(t, rolePermStore.Create(ctx, &model.RolePermission{
|
||||
RoleID: testRole.ID,
|
||||
PermID: testPerm.ID,
|
||||
}))
|
||||
|
||||
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: testUser.ID,
|
||||
UserType: testUser.UserType,
|
||||
})
|
||||
|
||||
cacheKey := constants.RedisUserPermissionsKey(testUser.ID)
|
||||
cachedData, err := rdb.Get(ctx, cacheKey).Result()
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, cachedData)
|
||||
|
||||
hasPermission, err := permSvc.CheckPermission(ctx, testUser.ID, "test:read", constants.PlatformWeb)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
|
||||
cachedData, err = rdb.Get(ctx, cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, cachedData)
|
||||
|
||||
type cacheItem struct {
|
||||
PermCode string `json:"perm_code"`
|
||||
Platform string `json:"platform"`
|
||||
}
|
||||
var cached []cacheItem
|
||||
require.NoError(t, json.Unmarshal([]byte(cachedData), &cached))
|
||||
assert.Len(t, cached, 1)
|
||||
assert.Equal(t, "test:read", cached[0].PermCode)
|
||||
assert.Equal(t, constants.PlatformWeb, cached[0].Platform)
|
||||
|
||||
hasPermission2, err := permSvc.CheckPermission(ctx, testUser.ID, "test:read", constants.PlatformWeb)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission2)
|
||||
}
|
||||
|
||||
func TestPermissionCache_ExpiredAfter30Minutes(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
|
||||
permSvc := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
testUser := &model.Account{
|
||||
Username: "testuser2",
|
||||
Phone: "13900000002",
|
||||
Password: "Test@123456",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, accountStore.Create(ctx, testUser))
|
||||
|
||||
testRole := &model.Role{
|
||||
RoleName: "测试角色2",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, roleStore.Create(ctx, testRole))
|
||||
|
||||
testPerm := &model.Permission{
|
||||
PermName: "测试权限2",
|
||||
PermCode: "test:write",
|
||||
PermType: constants.PermissionTypeButton,
|
||||
Platform: constants.PlatformWeb,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, permStore.Create(ctx, testPerm))
|
||||
|
||||
require.NoError(t, accountRoleStore.Create(ctx, &model.AccountRole{
|
||||
AccountID: testUser.ID,
|
||||
RoleID: testRole.ID,
|
||||
}))
|
||||
|
||||
require.NoError(t, rolePermStore.Create(ctx, &model.RolePermission{
|
||||
RoleID: testRole.ID,
|
||||
PermID: testPerm.ID,
|
||||
}))
|
||||
|
||||
ctx = middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: testUser.ID,
|
||||
UserType: testUser.UserType,
|
||||
})
|
||||
|
||||
hasPermission, err := permSvc.CheckPermission(ctx, testUser.ID, "test:write", constants.PlatformWeb)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
|
||||
cacheKey := constants.RedisUserPermissionsKey(testUser.ID)
|
||||
ttl, err := rdb.TTL(ctx, cacheKey).Result()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ttl > 29*time.Minute && ttl <= 30*time.Minute)
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/permission"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createContextWithUserType(userID uint, userType int) context.Context {
|
||||
return middleware.SetUserContext(context.Background(), &middleware.UserContextInfo{
|
||||
UserID: userID,
|
||||
UserType: userType,
|
||||
ShopID: 0,
|
||||
EnterpriseID: 0,
|
||||
CustomerID: 0,
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionService_CheckPermission_SuperAdmin(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
t.Run("超级管理员自动拥有所有权限", func(t *testing.T) {
|
||||
ctx := createContextWithUserType(1, constants.UserTypeSuperAdmin)
|
||||
|
||||
hasPermission, err := service.CheckPermission(ctx, 1, "any:permission", constants.PlatformAll)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionService_CheckPermission_NormalUser(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := createContextWithUserType(100, constants.UserTypePlatform)
|
||||
|
||||
perm1 := &model.Permission{
|
||||
PermName: "用户创建",
|
||||
PermCode: "user:create",
|
||||
PermType: constants.PermissionTypeButton,
|
||||
Platform: constants.PlatformAll,
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := permStore.Create(ctx, perm1)
|
||||
require.NoError(t, err)
|
||||
|
||||
perm2 := &model.Permission{
|
||||
PermName: "用户查看",
|
||||
PermCode: "user:view",
|
||||
PermType: constants.PermissionTypeButton,
|
||||
Platform: constants.PlatformWeb,
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = permStore.Create(ctx, perm2)
|
||||
require.NoError(t, err)
|
||||
|
||||
role := &model.Role{
|
||||
RoleName: "测试角色",
|
||||
RoleDesc: "测试用角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = roleStore.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: 100,
|
||||
RoleID: role.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
err = accountRoleStore.Create(ctx, accountRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
rolePerm1 := &model.RolePermission{
|
||||
RoleID: role.ID,
|
||||
PermID: perm1.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = rolePermStore.Create(ctx, rolePerm1)
|
||||
require.NoError(t, err)
|
||||
|
||||
rolePerm2 := &model.RolePermission{
|
||||
RoleID: role.ID,
|
||||
PermID: perm2.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = rolePermStore.Create(ctx, rolePerm2)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("有权限的用户应返回true", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 100, "user:create", constants.PlatformAll)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("无权限的用户应返回false", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 100, "user:delete", constants.PlatformAll)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("platform为all的权限在web端可访问", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 100, "user:create", constants.PlatformWeb)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("platform为web的权限在h5端不可访问", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 100, "user:view", constants.PlatformH5)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
|
||||
t.Run("platform为web的权限在web端可访问", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 100, "user:view", constants.PlatformWeb)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionService_CheckPermission_NoRole(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
t.Run("用户无角色应返回false", func(t *testing.T) {
|
||||
ctx := createContextWithUserType(200, constants.UserTypePlatform)
|
||||
|
||||
hasPermission, err := service.CheckPermission(ctx, 200, "any:permission", constants.PlatformAll)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionService_CheckPermission_RoleNoPermission(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := permission.New(permStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := createContextWithUserType(300, constants.UserTypePlatform)
|
||||
|
||||
role := &model.Role{
|
||||
RoleName: "空角色",
|
||||
RoleDesc: "无权限的角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := roleStore.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: 300,
|
||||
RoleID: role.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
err = accountRoleStore.Create(ctx, accountRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("角色无权限应返回false", func(t *testing.T) {
|
||||
hasPermission, err := service.CheckPermission(ctx, 300, "any:permission", constants.PlatformAll)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, hasPermission)
|
||||
})
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/permission"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestPermissionPlatformFilter_List 测试权限列表按 platform 过滤
|
||||
func TestPermissionPlatformFilter_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
baseReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000}
|
||||
_, existingTotal, err := service.List(ctx, baseReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
allReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformAll}
|
||||
_, existingAllTotal, err := service.List(ctx, allReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
webReq := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformWeb}
|
||||
_, existingWebTotal, err := service.List(ctx, webReq)
|
||||
require.NoError(t, err)
|
||||
|
||||
h5Req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformH5}
|
||||
_, existingH5Total, err := service.List(ctx, h5Req)
|
||||
require.NoError(t, err)
|
||||
|
||||
permissions := []*model.Permission{
|
||||
{PermName: "全端菜单_test", PermCode: "menu:all:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformAll, Status: constants.StatusEnabled},
|
||||
{PermName: "Web菜单_test", PermCode: "menu:web:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5菜单_test", PermCode: "menu:h5:test", PermType: constants.PermissionTypeMenu, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
{PermName: "Web按钮_test", PermCode: "button:web:test", PermType: constants.PermissionTypeButton, Platform: constants.PlatformWeb, Status: constants.StatusEnabled},
|
||||
{PermName: "H5按钮_test", PermCode: "button:h5:test", PermType: constants.PermissionTypeButton, Platform: constants.PlatformH5, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, perm := range permissions {
|
||||
require.NoError(t, tx.Create(perm).Error)
|
||||
}
|
||||
|
||||
t.Run("查询全部权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000}
|
||||
_, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingTotal+5, total)
|
||||
})
|
||||
|
||||
t.Run("只查询all端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformAll}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingAllTotal+1, total)
|
||||
found := false
|
||||
for _, perm := range perms {
|
||||
if perm.PermName == "全端菜单_test" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "应包含测试创建的全端菜单权限")
|
||||
})
|
||||
|
||||
t.Run("只查询web端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformWeb}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingWebTotal+2, total)
|
||||
for _, perm := range perms {
|
||||
assert.Equal(t, constants.PlatformWeb, perm.Platform)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("只查询h5端口权限", func(t *testing.T) {
|
||||
req := &dto.PermissionListRequest{Page: 1, PageSize: 1000, Platform: constants.PlatformH5}
|
||||
perms, total, err := service.List(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, existingH5Total+2, total)
|
||||
for _, perm := range perms {
|
||||
assert.Equal(t, constants.PlatformH5, perm.Platform)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPermissionPlatformFilter_CreateWithDefaultPlatform 测试创建权限时默认 platform 为 all
|
||||
func TestPermissionPlatformFilter_CreateWithDefaultPlatform(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建权限时不指定 platform
|
||||
req := &dto.CreatePermissionRequest{
|
||||
PermName: "测试权限",
|
||||
PermCode: "test:permission",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
// Platform 字段为空
|
||||
}
|
||||
|
||||
perm, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.PlatformAll, perm.Platform, "未指定 platform 时应默认为 all")
|
||||
}
|
||||
|
||||
// TestPermissionPlatformFilter_CreateWithSpecificPlatform 测试创建权限时指定 platform
|
||||
func TestPermissionPlatformFilter_CreateWithSpecificPlatform(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
platform string
|
||||
expected string
|
||||
}{
|
||||
{name: "指定为all", platform: constants.PlatformAll, expected: constants.PlatformAll},
|
||||
{name: "指定为web", platform: constants.PlatformWeb, expected: constants.PlatformWeb},
|
||||
{name: "指定为h5", platform: constants.PlatformH5, expected: constants.PlatformH5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := &dto.CreatePermissionRequest{
|
||||
PermName: "测试权限_" + tt.platform,
|
||||
PermCode: "test:" + tt.platform,
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Platform: tt.platform,
|
||||
}
|
||||
|
||||
perm, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, perm.Platform)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionPlatformFilter_Tree 测试权限树包含 platform 字段
|
||||
func TestPermissionPlatformFilter_Tree(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := permission.New(permissionStore, accountRoleStore, rolePermStore, nil, rdb)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
existingTree, err := service.GetTree(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
existingCount := len(existingTree)
|
||||
|
||||
parent := &model.Permission{
|
||||
PermName: "系统管理_tree_test",
|
||||
PermCode: "system:manage:tree_test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Platform: constants.PlatformWeb,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(parent).Error)
|
||||
|
||||
child := &model.Permission{
|
||||
PermName: "用户管理_tree_test",
|
||||
PermCode: "user:manage:tree_test",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Platform: constants.PlatformWeb,
|
||||
ParentID: &parent.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(child).Error)
|
||||
|
||||
tree, err := service.GetTree(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, tree, existingCount+1)
|
||||
|
||||
var testRoot *dto.PermissionTreeNode
|
||||
for _, node := range tree {
|
||||
if node.PermName == "系统管理_tree_test" {
|
||||
testRoot = node
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, testRoot, "应包含测试创建的父节点")
|
||||
assert.Equal(t, constants.PlatformWeb, testRoot.Platform)
|
||||
|
||||
require.Len(t, testRoot.Children, 1)
|
||||
childNode := testRoot.Children[0]
|
||||
assert.Equal(t, "用户管理_tree_test", childNode.PermName)
|
||||
assert.Equal(t, constants.PlatformWeb, childNode.Platform)
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func TestPermissionStore_List_AvailableForRoleTypes(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPermissionStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
platformPerm := &model.Permission{
|
||||
PermName: "平台专用权限",
|
||||
PermCode: "platform:only",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, platformPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
customerPerm := &model.Permission{
|
||||
PermName: "客户专用权限",
|
||||
PermCode: "customer:only",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, customerPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
commonPerm := &model.Permission{
|
||||
PermName: "通用权限",
|
||||
PermCode: "common:perm",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "1,2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, commonPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("过滤平台角色可用权限", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"available_for_role_type": 1,
|
||||
}
|
||||
perms, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "platform:only")
|
||||
assert.Contains(t, codes, "common:perm")
|
||||
assert.NotContains(t, codes, "customer:only")
|
||||
})
|
||||
|
||||
t.Run("过滤客户角色可用权限", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"available_for_role_type": 2,
|
||||
}
|
||||
perms, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "customer:only")
|
||||
assert.Contains(t, codes, "common:perm")
|
||||
assert.NotContains(t, codes, "platform:only")
|
||||
})
|
||||
|
||||
t.Run("不过滤时返回所有权限", func(t *testing.T) {
|
||||
perms, _, err := store.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "platform:only")
|
||||
assert.Contains(t, codes, "customer:only")
|
||||
assert.Contains(t, codes, "common:perm")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionStore_GetAll_AvailableForRoleType(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPermissionStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
platformPerm := &model.Permission{
|
||||
PermName: "平台菜单",
|
||||
PermCode: "platform:menu",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, platformPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
customerPerm := &model.Permission{
|
||||
PermName: "客户菜单",
|
||||
PermCode: "customer:menu",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, customerPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("GetAll按平台角色类型过滤", func(t *testing.T) {
|
||||
roleType := 1
|
||||
perms, err := store.GetAll(ctx, &roleType, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "platform:menu")
|
||||
assert.NotContains(t, codes, "customer:menu")
|
||||
})
|
||||
|
||||
t.Run("GetAll按客户角色类型过滤", func(t *testing.T) {
|
||||
roleType := 2
|
||||
perms, err := store.GetAll(ctx, &roleType, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "customer:menu")
|
||||
assert.NotContains(t, codes, "platform:menu")
|
||||
})
|
||||
|
||||
t.Run("GetAll不过滤时返回所有", func(t *testing.T) {
|
||||
perms, err := store.GetAll(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "platform:menu")
|
||||
assert.Contains(t, codes, "customer:menu")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPermissionStore_GetByPlatform_AvailableForRoleType(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewPermissionStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
webPlatformPerm := &model.Permission{
|
||||
PermName: "Web平台权限",
|
||||
PermCode: "web:platform",
|
||||
PermType: 1,
|
||||
Platform: "web",
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, webPlatformPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
h5CustomerPerm := &model.Permission{
|
||||
PermName: "H5客户权限",
|
||||
PermCode: "h5:customer",
|
||||
PermType: 1,
|
||||
Platform: "h5",
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, h5CustomerPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("同时按平台和角色类型过滤", func(t *testing.T) {
|
||||
roleType := 1
|
||||
perms, err := store.GetByPlatform(ctx, "web", &roleType)
|
||||
require.NoError(t, err)
|
||||
|
||||
var codes []string
|
||||
for _, p := range perms {
|
||||
codes = append(codes, p.PermCode)
|
||||
}
|
||||
assert.Contains(t, codes, "web:platform")
|
||||
assert.NotContains(t, codes, "h5:customer")
|
||||
})
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/account"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/account_audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestRoleAssignmentLimit_PlatformUser 测试平台用户可以分配多个角色(无限制)
|
||||
func TestRoleAssignmentLimit_PlatformUser(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
auditLogStore := postgres.NewAccountOperationLogStore(tx)
|
||||
auditService := account_audit.NewService(auditLogStore)
|
||||
service := account.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建平台用户
|
||||
platformUser := &model.Account{
|
||||
Username: "platform_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(platformUser).Error)
|
||||
|
||||
// 创建 3 个平台角色
|
||||
roles := []*model.Role{
|
||||
{RoleName: "运营", RoleType: constants.RoleTypePlatform, Status: constants.StatusEnabled},
|
||||
{RoleName: "客服", RoleType: constants.RoleTypePlatform, Status: constants.StatusEnabled},
|
||||
{RoleName: "财务", RoleType: constants.RoleTypePlatform, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, role := range roles {
|
||||
require.NoError(t, tx.Create(role).Error)
|
||||
}
|
||||
|
||||
// 为平台用户分配 3 个角色(应该成功,因为平台用户无限制)
|
||||
roleIDs := []uint{roles[0].ID, roles[1].ID, roles[2].ID}
|
||||
ars, err := service.AssignRoles(ctx, platformUser.ID, roleIDs)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 3)
|
||||
}
|
||||
|
||||
// TestRoleAssignmentLimit_AgentUser 测试代理账号只能分配一个角色
|
||||
func TestRoleAssignmentLimit_AgentUser(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
auditLogStore := postgres.NewAccountOperationLogStore(tx)
|
||||
auditService := account_audit.NewService(auditLogStore)
|
||||
service := account.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建代理账号
|
||||
agentAccount := &model.Account{
|
||||
Username: "agent_user",
|
||||
Phone: "13800000002",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeAgent,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(agentAccount).Error)
|
||||
|
||||
// 创建 2 个客户角色
|
||||
roles := []*model.Role{
|
||||
{RoleName: "一级代理", RoleType: constants.RoleTypeCustomer, Status: constants.StatusEnabled},
|
||||
{RoleName: "二级代理", RoleType: constants.RoleTypeCustomer, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, role := range roles {
|
||||
require.NoError(t, tx.Create(role).Error)
|
||||
}
|
||||
|
||||
// 先分配第一个角色(应该成功)
|
||||
ars, err := service.AssignRoles(ctx, agentAccount.ID, []uint{roles[0].ID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 1)
|
||||
|
||||
// 尝试分配第二个角色(应该失败,超过数量限制)
|
||||
_, err = service.AssignRoles(ctx, agentAccount.ID, []uint{roles[1].ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "最多只能分配 1 个角色")
|
||||
}
|
||||
|
||||
// TestRoleAssignmentLimit_EnterpriseUser 测试企业账号只能分配一个角色
|
||||
func TestRoleAssignmentLimit_EnterpriseUser(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
auditLogStore := postgres.NewAccountOperationLogStore(tx)
|
||||
auditService := account_audit.NewService(auditLogStore)
|
||||
service := account.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建企业账号
|
||||
enterpriseAccount := &model.Account{
|
||||
Username: "enterprise_user",
|
||||
Phone: "13800000003",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeEnterprise,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(enterpriseAccount).Error)
|
||||
|
||||
// 创建 2 个客户角色
|
||||
roles := []*model.Role{
|
||||
{RoleName: "企业普通", RoleType: constants.RoleTypeCustomer, Status: constants.StatusEnabled},
|
||||
{RoleName: "企业高级", RoleType: constants.RoleTypeCustomer, Status: constants.StatusEnabled},
|
||||
}
|
||||
for _, role := range roles {
|
||||
require.NoError(t, tx.Create(role).Error)
|
||||
}
|
||||
|
||||
// 先分配第一个角色(应该成功)
|
||||
ars, err := service.AssignRoles(ctx, enterpriseAccount.ID, []uint{roles[0].ID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, ars, 1)
|
||||
|
||||
// 尝试分配第二个角色(应该失败,超过数量限制)
|
||||
_, err = service.AssignRoles(ctx, enterpriseAccount.ID, []uint{roles[1].ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "最多只能分配 1 个角色")
|
||||
}
|
||||
|
||||
// TestRoleAssignmentLimit_SuperAdmin 测试超级管理员不允许分配角色
|
||||
func TestRoleAssignmentLimit_SuperAdmin(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
enterpriseStore := postgres.NewEnterpriseStore(tx, rdb)
|
||||
auditLogStore := postgres.NewAccountOperationLogStore(tx)
|
||||
auditService := account_audit.NewService(auditLogStore)
|
||||
service := account.New(accountStore, roleStore, accountRoleStore, shopRoleStore, shopStore, enterpriseStore, auditService)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = middleware.SetUserContext(ctx, middleware.NewSimpleUserContext(1, constants.UserTypeSuperAdmin, 0))
|
||||
|
||||
// 创建超级管理员
|
||||
superAdmin := &model.Account{
|
||||
Username: "superadmin",
|
||||
Phone: "13800000004",
|
||||
Password: "hashedpassword",
|
||||
UserType: constants.UserTypeSuperAdmin,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(superAdmin).Error)
|
||||
|
||||
// 创建一个平台角色
|
||||
role := &model.Role{
|
||||
RoleName: "测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
require.NoError(t, tx.Create(role).Error)
|
||||
|
||||
// 尝试为超级管理员分配角色(应该失败)
|
||||
_, err := service.AssignRoles(ctx, superAdmin.ID, []uint{role.ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "超级管理员不允许分配角色")
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/service/role"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func TestRoleService_AssignPermissions_ValidateAvailableForRoleTypes(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := role.New(roleStore, permStore, rolePermStore)
|
||||
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
platformRole := &model.Role{
|
||||
RoleName: "平台管理员",
|
||||
RoleDesc: "平台角色",
|
||||
RoleType: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := roleStore.Create(ctx, platformRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
customerRole := &model.Role{
|
||||
RoleName: "客户管理员",
|
||||
RoleDesc: "客户角色",
|
||||
RoleType: 2,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = roleStore.Create(ctx, customerRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
platformPerm := &model.Permission{
|
||||
PermName: "平台权限",
|
||||
PermCode: "platform:manage",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "1",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = permStore.Create(ctx, platformPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
customerPerm := &model.Permission{
|
||||
PermName: "客户权限",
|
||||
PermCode: "customer:manage",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = permStore.Create(ctx, customerPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
commonPerm := &model.Permission{
|
||||
PermName: "通用权限",
|
||||
PermCode: "common:view",
|
||||
PermType: 1,
|
||||
Platform: "all",
|
||||
AvailableForRoleTypes: "1,2",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = permStore.Create(ctx, commonPerm)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("为平台角色分配平台权限-成功", func(t *testing.T) {
|
||||
rps, err := service.AssignPermissions(ctx, platformRole.ID, []uint{platformPerm.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rps)
|
||||
})
|
||||
|
||||
t.Run("为平台角色分配通用权限-成功", func(t *testing.T) {
|
||||
rps, err := service.AssignPermissions(ctx, platformRole.ID, []uint{commonPerm.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rps)
|
||||
})
|
||||
|
||||
t.Run("为平台角色分配客户专用权限-失败", func(t *testing.T) {
|
||||
_, err := service.AssignPermissions(ctx, platformRole.ID, []uint{customerPerm.ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "不适用于此角色类型")
|
||||
})
|
||||
|
||||
t.Run("为客户角色分配客户权限-成功", func(t *testing.T) {
|
||||
rps, err := service.AssignPermissions(ctx, customerRole.ID, []uint{customerPerm.ID})
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rps)
|
||||
})
|
||||
|
||||
t.Run("为客户角色分配平台专用权限-失败", func(t *testing.T) {
|
||||
_, err := service.AssignPermissions(ctx, customerRole.ID, []uint{platformPerm.ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "不适用于此角色类型")
|
||||
})
|
||||
|
||||
t.Run("批量分配权限时部分不匹配-失败", func(t *testing.T) {
|
||||
_, err := service.AssignPermissions(ctx, platformRole.ID, []uint{platformPerm.ID, customerPerm.ID})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "不适用于此角色类型")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRoleService_UpdateStatus(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
permStore := postgres.NewPermissionStore(tx)
|
||||
rolePermStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
service := role.New(roleStore, permStore, rolePermStore)
|
||||
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
testRole := &model.Role{
|
||||
RoleName: "测试角色",
|
||||
RoleDesc: "用于测试状态切换",
|
||||
RoleType: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := roleStore.Create(ctx, testRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("禁用角色", func(t *testing.T) {
|
||||
err := service.UpdateStatus(ctx, testRole.ID, constants.StatusDisabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
role, err := roleStore.GetByID(ctx, testRole.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, role.Status)
|
||||
})
|
||||
|
||||
t.Run("启用角色", func(t *testing.T) {
|
||||
err := service.UpdateStatus(ctx, testRole.ID, constants.StatusEnabled)
|
||||
require.NoError(t, err)
|
||||
|
||||
role, err := roleStore.GetByID(ctx, testRole.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, role.Status)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的角色-失败", func(t *testing.T) {
|
||||
err := service.UpdateStatus(ctx, 99999, constants.StatusEnabled)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "角色不存在")
|
||||
})
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// TestIsRoleTypeMatchUserType 测试角色类型与用户类型匹配规则
|
||||
func TestIsRoleTypeMatchUserType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
roleType int
|
||||
userType int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "超级管理员不需要角色",
|
||||
roleType: constants.RoleTypePlatform,
|
||||
userType: constants.UserTypeSuperAdmin,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "平台用户匹配平台角色",
|
||||
roleType: constants.RoleTypePlatform,
|
||||
userType: constants.UserTypePlatform,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "平台用户不匹配客户角色",
|
||||
roleType: constants.RoleTypeCustomer,
|
||||
userType: constants.UserTypePlatform,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "代理账号匹配客户角色",
|
||||
roleType: constants.RoleTypeCustomer,
|
||||
userType: constants.UserTypeAgent,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "代理账号不匹配平台角色",
|
||||
roleType: constants.RoleTypePlatform,
|
||||
userType: constants.UserTypeAgent,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "企业账号匹配客户角色",
|
||||
roleType: constants.RoleTypeCustomer,
|
||||
userType: constants.UserTypeEnterprise,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "企业账号不匹配平台角色",
|
||||
roleType: constants.RoleTypePlatform,
|
||||
userType: constants.UserTypeEnterprise,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := constants.IsRoleTypeMatchUserType(tt.roleType, tt.userType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetMaxRolesForUserType 测试用户类型的最大角色数量限制
|
||||
func TestGetMaxRolesForUserType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
userType int
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "超级管理员不需要角色",
|
||||
userType: constants.UserTypeSuperAdmin,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "平台用户无角色数量限制",
|
||||
userType: constants.UserTypePlatform,
|
||||
expected: -1, // -1 表示无限制
|
||||
},
|
||||
{
|
||||
name: "代理账号最多一个角色",
|
||||
userType: constants.UserTypeAgent,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "企业账号最多一个角色",
|
||||
userType: constants.UserTypeEnterprise,
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
name: "未知用户类型不允许角色",
|
||||
userType: 999,
|
||||
expected: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := constants.GetMaxRolesForUserType(tt.userType)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"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/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
func createCommissionTestContext(userID uint) context.Context {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserID, userID)
|
||||
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func TestShopCommissionService_ListShopCommissionSummary(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
|
||||
service := shop_commission.New(shopStore, accountStore, walletStore, commissionWithdrawalRequestStore, commissionRecordStore)
|
||||
|
||||
t.Run("查询店铺佣金汇总列表", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "COMMISSION_TEST_001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.ShopCommissionSummaryListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListShopCommissionSummary(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("按店铺名称筛选", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "筛选测试店铺",
|
||||
ShopCode: "FILTER_TEST_001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.ShopCommissionSummaryListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
ShopName: "筛选测试",
|
||||
}
|
||||
|
||||
result, err := service.ListShopCommissionSummary(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopCommissionService_ListShopWithdrawalRequests(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
|
||||
service := shop_commission.New(shopStore, accountStore, walletStore, commissionWithdrawalRequestStore, commissionRecordStore)
|
||||
|
||||
t.Run("查询店铺提现记录", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "提现测试店铺",
|
||||
ShopCode: "WITHDRAWAL_TEST_001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000003",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.ShopWithdrawalRequestListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListShopWithdrawalRequests(ctx, shop.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询不存在的店铺提现记录应失败", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
req := &dto.ShopWithdrawalRequestListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
_, err := service.ListShopWithdrawalRequests(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShopCommissionService_ListShopCommissionRecords(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||
commissionWithdrawalRequestStore := postgres.NewCommissionWithdrawalRequestStore(tx, rdb)
|
||||
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||
|
||||
service := shop_commission.New(shopStore, accountStore, walletStore, commissionWithdrawalRequestStore, commissionRecordStore)
|
||||
|
||||
t.Run("查询店铺佣金明细", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
shop := &model.Shop{
|
||||
ShopName: "佣金明细测试店铺",
|
||||
ShopCode: "RECORD_TEST_001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000004",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := &dto.ShopCommissionRecordListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
result, err := service.ListShopCommissionRecords(ctx, shop.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.GreaterOrEqual(t, result.Total, int64(0))
|
||||
})
|
||||
|
||||
t.Run("查询不存在的店铺佣金明细应失败", func(t *testing.T) {
|
||||
ctx := createCommissionTestContext(1)
|
||||
|
||||
req := &dto.ShopCommissionRecordListReq{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
_, err := service.ListShopCommissionRecords(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuildShopHierarchyPath(t *testing.T) {
|
||||
t.Run("一级店铺路径", func(t *testing.T) {
|
||||
shop := &model.Shop{
|
||||
ShopName: "一级店铺",
|
||||
Level: 1,
|
||||
ParentID: nil,
|
||||
}
|
||||
path := buildTestHierarchyPath(shop, nil)
|
||||
assert.Equal(t, "一级店铺", path)
|
||||
})
|
||||
|
||||
t.Run("多级店铺路径", func(t *testing.T) {
|
||||
parentID := uint(1)
|
||||
shop := &model.Shop{
|
||||
ShopName: "二级店铺",
|
||||
Level: 2,
|
||||
ParentID: &parentID,
|
||||
}
|
||||
parent := &model.Shop{
|
||||
ShopName: "一级店铺",
|
||||
Level: 1,
|
||||
ParentID: nil,
|
||||
}
|
||||
path := buildTestHierarchyPath(shop, parent)
|
||||
assert.Equal(t, "一级店铺 > 二级店铺", path)
|
||||
})
|
||||
}
|
||||
|
||||
func buildTestHierarchyPath(shop *model.Shop, parent *model.Shop) string {
|
||||
if parent == nil {
|
||||
return shop.ShopName
|
||||
}
|
||||
return parent.ShopName + " > " + shop.ShopName
|
||||
}
|
||||
@@ -1,765 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
"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/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestShopService_Create 测试创建店铺
|
||||
func TestShopService_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("创建一级店铺成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
req := &dto.CreateShopRequest{
|
||||
ShopName: "测试一级店铺",
|
||||
ShopCode: "SHOP_L1_001",
|
||||
ParentID: nil,
|
||||
ContactName: "张三",
|
||||
ContactPhone: "13800000001",
|
||||
Province: "北京市",
|
||||
City: "北京市",
|
||||
District: "朝阳区",
|
||||
Address: "朝阳路100号",
|
||||
InitUsername: generateUniqueUsername("admin", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, result.ID)
|
||||
assert.Equal(t, "测试一级店铺", result.ShopName)
|
||||
assert.Equal(t, "SHOP_L1_001", result.ShopCode)
|
||||
assert.Equal(t, 1, result.Level)
|
||||
assert.Nil(t, result.ParentID)
|
||||
assert.Equal(t, constants.StatusEnabled, result.Status)
|
||||
})
|
||||
|
||||
t.Run("创建二级店铺成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 先创建一级店铺
|
||||
parent := &model.Shop{
|
||||
ShopName: "一级店铺",
|
||||
ShopCode: "PARENT_001",
|
||||
Level: 1,
|
||||
ContactName: "李四",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, parent)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建二级店铺
|
||||
req := &dto.CreateShopRequest{
|
||||
ShopName: "测试二级店铺",
|
||||
ShopCode: "SHOP_L2_001",
|
||||
ParentID: &parent.ID,
|
||||
ContactName: "王五",
|
||||
ContactPhone: "13800000003",
|
||||
InitUsername: generateUniqueUsername("agent", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, result.ID)
|
||||
assert.Equal(t, 2, result.Level)
|
||||
assert.Equal(t, parent.ID, *result.ParentID)
|
||||
})
|
||||
|
||||
t.Run("层级校验-创建第8级店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建 7 级店铺层级
|
||||
var shops []*model.Shop
|
||||
for i := 1; i <= 7; i++ {
|
||||
var parentID *uint
|
||||
if i > 1 {
|
||||
parentID = &shops[i-2].ID
|
||||
}
|
||||
|
||||
shopModel := &model.Shop{
|
||||
ShopName: testutils.GenerateUsername("店铺L", i),
|
||||
ShopCode: testutils.GenerateUsername("LEVEL", i),
|
||||
ParentID: parentID,
|
||||
Level: i,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: testutils.GeneratePhone("138", i),
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
shops = append(shops, shopModel)
|
||||
}
|
||||
|
||||
// 验证已创建 7 级
|
||||
assert.Len(t, shops, 7)
|
||||
assert.Equal(t, 7, shops[6].Level)
|
||||
|
||||
// 尝试创建第 8 级店铺(应该失败)
|
||||
req := &dto.CreateShopRequest{
|
||||
ShopName: "第8级店铺",
|
||||
ShopCode: "SHOP_L8_001",
|
||||
ParentID: &shops[6].ID, // 第7级店铺的ID
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000008",
|
||||
InitUsername: generateUniqueUsername("level8", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok, "错误应该是 AppError 类型")
|
||||
assert.Equal(t, errors.CodeShopLevelExceeded, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "不能超过 7 级")
|
||||
})
|
||||
|
||||
t.Run("店铺编号唯一性检查-重复编号应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建第一个店铺
|
||||
req1 := &dto.CreateShopRequest{
|
||||
ShopName: "店铺A",
|
||||
ShopCode: "UNIQUE_CODE_001",
|
||||
ContactName: "张三",
|
||||
ContactPhone: "13800000001",
|
||||
InitUsername: generateUniqueUsername("unique1", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
_, err := service.Create(ctx, req1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 尝试创建相同编号的店铺(应该失败)
|
||||
req2 := &dto.CreateShopRequest{
|
||||
ShopName: "店铺B",
|
||||
ShopCode: "UNIQUE_CODE_001", // 重复编号
|
||||
ContactName: "李四",
|
||||
ContactPhone: "13800000002",
|
||||
InitUsername: generateUniqueUsername("unique2", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
result, err := service.Create(ctx, req2)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopCodeExists, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "编号已存在")
|
||||
})
|
||||
|
||||
t.Run("上级店铺不存在应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
nonExistentID := uint(99999)
|
||||
req := &dto.CreateShopRequest{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "SHOP_INVALID_PARENT",
|
||||
ParentID: &nonExistentID, // 不存在的上级店铺 ID
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000009",
|
||||
InitUsername: generateUniqueUsername("invalid", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeInvalidParentID, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "上级店铺不存在")
|
||||
})
|
||||
|
||||
t.Run("未授权访问应失败", func(t *testing.T) {
|
||||
ctx := context.Background() // 没有用户 ID 的 context
|
||||
|
||||
req := &dto.CreateShopRequest{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "SHOP_UNAUTHORIZED",
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000010",
|
||||
InitUsername: generateUniqueUsername("unauth", t),
|
||||
InitPhone: generateUniquePhone(),
|
||||
InitPassword: "password123",
|
||||
}
|
||||
|
||||
result, err := service.Create(ctx, req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
assert.Contains(t, appErr.Message, "未授权")
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_Update 测试更新店铺
|
||||
func TestShopService_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("更新店铺信息成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 先创建店铺
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "原始店铺名称",
|
||||
ShopCode: "ORIGINAL_CODE",
|
||||
Level: 1,
|
||||
ContactName: "原联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 更新店铺
|
||||
req := &dto.UpdateShopRequest{
|
||||
ShopName: "更新后的店铺名称",
|
||||
ContactName: "新联系人",
|
||||
ContactPhone: "13900000001",
|
||||
Province: "上海市",
|
||||
City: "上海市",
|
||||
District: "浦东新区",
|
||||
Address: "陆家嘴环路1000号",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
result, err := service.Update(ctx, shopModel.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "更新后的店铺名称", result.ShopName)
|
||||
assert.Equal(t, "ORIGINAL_CODE", result.ShopCode)
|
||||
assert.Equal(t, "新联系人", result.ContactName)
|
||||
assert.Equal(t, "13900000001", result.ContactPhone)
|
||||
assert.Equal(t, "上海市", result.Province)
|
||||
assert.Equal(t, "上海市", result.City)
|
||||
assert.Equal(t, "浦东新区", result.District)
|
||||
assert.Equal(t, "陆家嘴环路1000号", result.Address)
|
||||
})
|
||||
|
||||
t.Run("更新店铺编号-唯一性检查", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建两个店铺
|
||||
shop1 := &model.Shop{
|
||||
ShopName: "店铺1",
|
||||
ShopCode: "CODE_001",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop1)
|
||||
require.NoError(t, err)
|
||||
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "店铺2",
|
||||
ShopCode: "CODE_002",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = shopStore.Create(ctx, shop2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 尝试更新 shop2 的名称为已存在的名称(应该成功,因为名称不需要唯一性)
|
||||
req := &dto.UpdateShopRequest{
|
||||
ShopName: "店铺1",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
result, err := service.Update(ctx, shop2.ID, req)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "店铺1", result.ShopName)
|
||||
})
|
||||
|
||||
t.Run("更新不存在的店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
req := &dto.UpdateShopRequest{
|
||||
ShopName: "新名称",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
result, err := service.Update(ctx, 99999, req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("未授权访问应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := &dto.UpdateShopRequest{
|
||||
ShopName: "新名称",
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
|
||||
result, err := service.Update(ctx, 1, req)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_Disable 测试禁用店铺
|
||||
func TestShopService_Disable(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("禁用店铺成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建店铺
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "待禁用店铺",
|
||||
ShopCode: "SHOP_TO_DISABLE",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, shopModel.Status)
|
||||
|
||||
// 禁用店铺
|
||||
err = service.Disable(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证状态已更新
|
||||
result, err := shopStore.GetByID(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, result.Status)
|
||||
assert.Equal(t, uint(1), result.Updater)
|
||||
})
|
||||
|
||||
t.Run("禁用不存在的店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
err := service.Disable(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("未授权访问应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
err := service.Disable(ctx, 1)
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_Enable 测试启用店铺
|
||||
func TestShopService_Enable(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("启用店铺成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建启用状态的店铺
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "待启用店铺",
|
||||
ShopCode: "SHOP_TO_ENABLE",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 先禁用店铺
|
||||
shopModel.Status = constants.StatusDisabled
|
||||
err = shopStore.Update(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证已禁用
|
||||
disabled, err := shopStore.GetByID(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, disabled.Status)
|
||||
|
||||
// 启用店铺
|
||||
err = service.Enable(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证状态已更新为启用
|
||||
result, err := shopStore.GetByID(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusEnabled, result.Status)
|
||||
assert.Equal(t, uint(1), result.Updater)
|
||||
})
|
||||
|
||||
t.Run("启用不存在的店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
err := service.Enable(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("未授权访问应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
err := service.Enable(ctx, 1)
|
||||
assert.Error(t, err)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_GetByID 测试获取店铺详情
|
||||
func TestShopService_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("获取存在的店铺", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建店铺
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "TEST_SHOP_001",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 获取店铺
|
||||
result, err := service.GetByID(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, shopModel.ID, result.ID)
|
||||
assert.Equal(t, "测试店铺", result.ShopName)
|
||||
assert.Equal(t, "TEST_SHOP_001", result.ShopCode)
|
||||
})
|
||||
|
||||
t.Run("获取不存在的店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
result, err := service.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, result)
|
||||
|
||||
// 验证错误码
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_List 测试查询店铺列表
|
||||
func TestShopService_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("查询店铺列表", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建多个店铺
|
||||
for i := 1; i <= 5; i++ {
|
||||
shopModel := &model.Shop{
|
||||
ShopName: testutils.GenerateUsername("测试店铺", i),
|
||||
ShopCode: testutils.GenerateUsername("SHOP_LIST", i),
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// 查询列表
|
||||
shops, total, err := service.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(shops), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_GetSubordinateShopIDs 测试获取下级店铺 ID 列表
|
||||
func TestShopService_GetSubordinateShopIDs(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("获取下级店铺 ID 列表", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
// 创建店铺层级
|
||||
shop1 := &model.Shop{
|
||||
ShopName: "一级店铺",
|
||||
ShopCode: "SUBORDINATE_L1",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shop1)
|
||||
require.NoError(t, err)
|
||||
|
||||
shop2 := &model.Shop{
|
||||
ShopName: "二级店铺",
|
||||
ShopCode: "SUBORDINATE_L2",
|
||||
ParentID: &shop1.ID,
|
||||
Level: 2,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = shopStore.Create(ctx, shop2)
|
||||
require.NoError(t, err)
|
||||
|
||||
shop3 := &model.Shop{
|
||||
ShopName: "三级店铺",
|
||||
ShopCode: "SUBORDINATE_L3",
|
||||
ParentID: &shop2.ID,
|
||||
Level: 3,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = shopStore.Create(ctx, shop3)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 获取一级店铺的所有下级(包含自己)
|
||||
ids, err := service.GetSubordinateShopIDs(ctx, shop1.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, ids, shop1.ID)
|
||||
assert.Contains(t, ids, shop2.ID)
|
||||
assert.Contains(t, ids, shop3.ID)
|
||||
assert.Len(t, ids, 3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopService_Delete 测试删除店铺
|
||||
func TestShopService_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
shopStore := postgres.NewShopStore(tx, rdb)
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
shopRoleStore := postgres.NewShopRoleStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
service := shop.New(shopStore, accountStore, shopRoleStore, roleStore)
|
||||
|
||||
t.Run("删除店铺成功", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "待删除店铺",
|
||||
ShopCode: "DELETE_001",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.Delete(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = shopStore.GetByID(ctx, shopModel.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("删除店铺并禁用关联账号", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
shopModel := &model.Shop{
|
||||
ShopName: "有账号的店铺",
|
||||
ShopCode: "DELETE_002",
|
||||
Level: 1,
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := shopStore.Create(ctx, shopModel)
|
||||
require.NoError(t, err)
|
||||
|
||||
account := &model.Account{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
Username: testutils.GenerateUsername("agent", 1),
|
||||
Phone: testutils.GeneratePhone("139", 1),
|
||||
Password: "hashedpassword123",
|
||||
UserType: constants.UserTypeAgent,
|
||||
ShopID: &shopModel.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err = accountStore.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = service.Delete(ctx, shopModel.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
updatedAccount, err := accountStore.GetByID(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, updatedAccount.Status)
|
||||
})
|
||||
|
||||
t.Run("删除不存在的店铺应失败", func(t *testing.T) {
|
||||
ctx := createContextWithUserID(1)
|
||||
|
||||
err := service.Delete(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeShopNotFound, appErr.Code)
|
||||
})
|
||||
|
||||
t.Run("未授权访问应失败", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
err := service.Delete(ctx, 1)
|
||||
assert.Error(t, err)
|
||||
|
||||
appErr, ok := err.(*errors.AppError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, errors.CodeUnauthorized, appErr.Code)
|
||||
})
|
||||
}
|
||||
@@ -1,452 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestShopStore_Create 测试创建店铺
|
||||
func TestShopStore_Create(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
shop *model.Shop
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "创建一级店铺",
|
||||
shop: &model.Shop{
|
||||
ShopName: "一级代理店铺",
|
||||
ShopCode: "SHOP001",
|
||||
ParentID: nil,
|
||||
Level: 1,
|
||||
ContactName: "张三",
|
||||
ContactPhone: "13800000001",
|
||||
Province: "北京市",
|
||||
City: "北京市",
|
||||
District: "朝阳区",
|
||||
Address: "朝阳路100号",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "创建带父店铺的店铺",
|
||||
shop: &model.Shop{
|
||||
ShopName: "二级代理店铺",
|
||||
ShopCode: "SHOP002",
|
||||
Level: 2,
|
||||
ContactName: "李四",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.shop.BaseModel.Creator = 1
|
||||
tt.shop.BaseModel.Updater = 1
|
||||
|
||||
err := store.Create(ctx, tt.shop)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, tt.shop.ID)
|
||||
assert.NotZero(t, tt.shop.CreatedAt)
|
||||
assert.NotZero(t, tt.shop.UpdatedAt)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestShopStore_GetByID 测试根据 ID 查询店铺
|
||||
func TestShopStore_GetByID(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "TEST001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询存在的店铺", func(t *testing.T) {
|
||||
found, err := store.GetByID(ctx, shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, shop.ShopName, found.ShopName)
|
||||
assert.Equal(t, shop.ShopCode, found.ShopCode)
|
||||
assert.Equal(t, shop.Level, found.Level)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的店铺", func(t *testing.T) {
|
||||
_, err := store.GetByID(ctx, 99999)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_GetByCode 测试根据店铺编号查询
|
||||
func TestShopStore_GetByCode(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "测试店铺",
|
||||
ShopCode: "UNIQUE001",
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("根据店铺编号查询", func(t *testing.T) {
|
||||
found, err := store.GetByCode(ctx, "UNIQUE001")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, shop.ID, found.ID)
|
||||
assert.Equal(t, shop.ShopName, found.ShopName)
|
||||
})
|
||||
|
||||
t.Run("查询不存在的店铺编号", func(t *testing.T) {
|
||||
_, err := store.GetByCode(ctx, "NONEXISTENT")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_Update 测试更新店铺
|
||||
func TestShopStore_Update(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "原始店铺名称",
|
||||
ShopCode: "UPDATE001",
|
||||
Level: 1,
|
||||
ContactName: "原联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("更新店铺信息", func(t *testing.T) {
|
||||
shop.ShopName = "更新后的店铺名称"
|
||||
shop.ContactName = "新联系人"
|
||||
shop.ContactPhone = "13900000001"
|
||||
shop.Updater = 2
|
||||
|
||||
err := store.Update(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证更新
|
||||
found, err := store.GetByID(ctx, shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "更新后的店铺名称", found.ShopName)
|
||||
assert.Equal(t, "新联系人", found.ContactName)
|
||||
assert.Equal(t, "13900000001", found.ContactPhone)
|
||||
assert.Equal(t, uint(2), found.Updater)
|
||||
})
|
||||
|
||||
t.Run("更新店铺状态", func(t *testing.T) {
|
||||
shop.Status = constants.StatusDisabled
|
||||
err := store.Update(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
found, err := store.GetByID(ctx, shop.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, constants.StatusDisabled, found.Status)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_Delete 测试软删除店铺
|
||||
func TestShopStore_Delete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "待删除店铺",
|
||||
ShopCode: "DELETE001",
|
||||
Level: 1,
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除店铺", func(t *testing.T) {
|
||||
err := store.Delete(ctx, shop.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证已被软删除(GetByID 应该找不到)
|
||||
_, err = store.GetByID(ctx, shop.ID)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_List 测试查询店铺列表
|
||||
func TestShopStore_List(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建多个测试店铺
|
||||
for i := 1; i <= 5; i++ {
|
||||
shop := &model.Shop{
|
||||
ShopName: testutils.GenerateUsername("测试店铺", i),
|
||||
ShopCode: testutils.GenerateUsername("SHOP", i),
|
||||
Level: 1,
|
||||
ContactName: "测试联系人",
|
||||
ContactPhone: testutils.GeneratePhone("138", i),
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("分页查询", func(t *testing.T) {
|
||||
shops, total, err := store.List(ctx, nil, nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(shops), 5)
|
||||
assert.GreaterOrEqual(t, total, int64(5))
|
||||
})
|
||||
|
||||
t.Run("带过滤条件查询", func(t *testing.T) {
|
||||
filters := map[string]interface{}{
|
||||
"level": 1,
|
||||
}
|
||||
shops, _, err := store.List(ctx, nil, filters)
|
||||
require.NoError(t, err)
|
||||
for _, shop := range shops {
|
||||
assert.Equal(t, 1, shop.Level)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_GetSubordinateShopIDs 测试递归查询下级店铺 ID
|
||||
func TestShopStore_GetSubordinateShopIDs(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建店铺层级结构
|
||||
// Level 1
|
||||
shop1 := &model.Shop{
|
||||
ShopName: "一级店铺",
|
||||
ShopCode: "L1_001",
|
||||
ParentID: nil,
|
||||
Level: 1,
|
||||
ContactName: "一级联系人",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Level 2 - 子店铺 1
|
||||
shop2_1 := &model.Shop{
|
||||
ShopName: "二级店铺1",
|
||||
ShopCode: "L2_001",
|
||||
ParentID: &shop1.ID,
|
||||
Level: 2,
|
||||
ContactName: "二级联系人1",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, shop2_1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Level 2 - 子店铺 2
|
||||
shop2_2 := &model.Shop{
|
||||
ShopName: "二级店铺2",
|
||||
ShopCode: "L2_002",
|
||||
ParentID: &shop1.ID,
|
||||
Level: 2,
|
||||
ContactName: "二级联系人2",
|
||||
ContactPhone: "13800000003",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, shop2_2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Level 3 - 孙店铺
|
||||
shop3 := &model.Shop{
|
||||
ShopName: "三级店铺",
|
||||
ShopCode: "L3_001",
|
||||
ParentID: &shop2_1.ID,
|
||||
Level: 3,
|
||||
ContactName: "三级联系人",
|
||||
ContactPhone: "13800000004",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err = store.Create(ctx, shop3)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("查询一级店铺的所有下级(包含自己)", func(t *testing.T) {
|
||||
ids, err := store.GetSubordinateShopIDs(ctx, shop1.ID)
|
||||
require.NoError(t, err)
|
||||
// 应该包含自己(shop1)和所有下级(shop2_1, shop2_2, shop3)
|
||||
assert.Contains(t, ids, shop1.ID)
|
||||
assert.Contains(t, ids, shop2_1.ID)
|
||||
assert.Contains(t, ids, shop2_2.ID)
|
||||
assert.Contains(t, ids, shop3.ID)
|
||||
assert.Len(t, ids, 4)
|
||||
})
|
||||
|
||||
t.Run("查询二级店铺的下级(包含自己)", func(t *testing.T) {
|
||||
ids, err := store.GetSubordinateShopIDs(ctx, shop2_1.ID)
|
||||
require.NoError(t, err)
|
||||
// 应该包含自己(shop2_1)和下级(shop3)
|
||||
assert.Contains(t, ids, shop2_1.ID)
|
||||
assert.Contains(t, ids, shop3.ID)
|
||||
assert.Len(t, ids, 2)
|
||||
})
|
||||
|
||||
t.Run("查询没有下级的店铺(只返回自己)", func(t *testing.T) {
|
||||
ids, err := store.GetSubordinateShopIDs(ctx, shop3.ID)
|
||||
require.NoError(t, err)
|
||||
// 应该只包含自己
|
||||
assert.Contains(t, ids, shop3.ID)
|
||||
assert.Len(t, ids, 1)
|
||||
})
|
||||
|
||||
t.Run("验证 Redis 缓存", func(t *testing.T) {
|
||||
// 第一次查询会写入缓存
|
||||
ids1, err := store.GetSubordinateShopIDs(ctx, shop1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 第二次查询应该从缓存读取(结果相同)
|
||||
ids2, err := store.GetSubordinateShopIDs(ctx, shop1.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ids1, ids2)
|
||||
assert.Len(t, ids2, 4) // 包含自己+3个下级
|
||||
})
|
||||
}
|
||||
|
||||
// TestShopStore_UniqueConstraints 测试唯一约束
|
||||
func TestShopStore_UniqueConstraints(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewShopStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试店铺
|
||||
shop := &model.Shop{
|
||||
ShopName: "唯一测试店铺",
|
||||
ShopCode: "UNIQUE_CODE",
|
||||
Level: 1,
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000001",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, shop)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("重复店铺编号应失败", func(t *testing.T) {
|
||||
duplicate := &model.Shop{
|
||||
ShopName: "另一个店铺",
|
||||
ShopCode: "UNIQUE_CODE", // 重复
|
||||
Level: 1,
|
||||
ContactName: "测试",
|
||||
ContactPhone: "13800000002",
|
||||
Status: constants.StatusEnabled,
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
},
|
||||
}
|
||||
err := store.Create(ctx, duplicate)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package unit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||
)
|
||||
|
||||
// TestAccountSoftDelete 测试账号软删除功能
|
||||
func TestAccountSoftDelete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
store := postgres.NewAccountStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "soft_delete_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除账号", func(t *testing.T) {
|
||||
err := store.Delete(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 正常查询应该找不到
|
||||
_, err = store.GetByID(ctx, account.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("使用 Unscoped 可以查到已删除账号", func(t *testing.T) {
|
||||
var found model.Account
|
||||
err := tx.Unscoped().First(&found, account.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, account.Username, found.Username)
|
||||
assert.NotNil(t, found.DeletedAt)
|
||||
})
|
||||
|
||||
t.Run("软删除后可以重用用户名和手机号", func(t *testing.T) {
|
||||
// 创建同名账号(因为原账号已软删除)
|
||||
newAccount := &model.Account{
|
||||
Username: "soft_delete_user", // 重用已删除账号的用户名
|
||||
Phone: "13800000001", // 重用已删除账号的手机号
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := store.Create(ctx, newAccount)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, account.ID, newAccount.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRoleSoftDelete 测试角色软删除功能
|
||||
func TestRoleSoftDelete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "test_role",
|
||||
RoleDesc: "测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := roleStore.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除角色", func(t *testing.T) {
|
||||
err := roleStore.Delete(ctx, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 正常查询应该找不到
|
||||
_, err = roleStore.GetByID(ctx, role.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("使用 Unscoped 可以查到已删除角色", func(t *testing.T) {
|
||||
var found model.Role
|
||||
err := tx.Unscoped().First(&found, role.ID).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, role.RoleName, found.RoleName)
|
||||
assert.NotNil(t, found.DeletedAt)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPermissionSoftDelete 测试权限软删除功能
|
||||
func TestPermissionSoftDelete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试权限
|
||||
permission := &model.Permission{
|
||||
PermName: "测试权限",
|
||||
PermCode: "test:permission",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := permissionStore.Create(ctx, permission)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除权限", func(t *testing.T) {
|
||||
err := permissionStore.Delete(ctx, permission.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 正常查询应该找不到
|
||||
_, err = permissionStore.GetByID(ctx, permission.ID)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
})
|
||||
|
||||
t.Run("软删除后可以重用权限码", func(t *testing.T) {
|
||||
newPermission := &model.Permission{
|
||||
PermName: "新测试权限",
|
||||
PermCode: "test:permission", // 重用已删除权限的 perm_code
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := permissionStore.Create(ctx, newPermission)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, permission.ID, newPermission.ID)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAccountRoleSoftDelete 测试账号-角色关联软删除功能
|
||||
func TestAccountRoleSoftDelete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
accountRoleStore := postgres.NewAccountRoleStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试账号
|
||||
account := &model.Account{
|
||||
Username: "ar_user",
|
||||
Phone: "13800000001",
|
||||
Password: "hashed_password",
|
||||
UserType: constants.UserTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := accountStore.Create(ctx, account)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "ar_role",
|
||||
RoleDesc: "测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err = roleStore.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建关联
|
||||
accountRole := &model.AccountRole{
|
||||
AccountID: account.ID,
|
||||
RoleID: role.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
err = accountRoleStore.Create(ctx, accountRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除账号-角色关联", func(t *testing.T) {
|
||||
err := accountRoleStore.Delete(ctx, account.ID, role.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 查询应该找不到
|
||||
roles, err := accountRoleStore.GetByAccountID(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, roles, 0)
|
||||
})
|
||||
|
||||
t.Run("软删除后可以重新关联", func(t *testing.T) {
|
||||
newAccountRole := &model.AccountRole{
|
||||
AccountID: account.ID,
|
||||
RoleID: role.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
Creator: 1,
|
||||
Updater: 1,
|
||||
}
|
||||
err := accountRoleStore.Create(ctx, newAccountRole)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证可以查询到
|
||||
roles, err := accountRoleStore.GetByAccountID(ctx, account.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, roles, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRolePermissionSoftDelete 测试角色-权限关联软删除功能
|
||||
func TestRolePermissionSoftDelete(t *testing.T) {
|
||||
tx := testutils.NewTestTransaction(t)
|
||||
rdb := testutils.GetTestRedis(t)
|
||||
testutils.CleanTestRedisKeys(t, rdb)
|
||||
|
||||
roleStore := postgres.NewRoleStore(tx)
|
||||
permissionStore := postgres.NewPermissionStore(tx)
|
||||
rolePermissionStore := postgres.NewRolePermissionStore(tx, rdb)
|
||||
ctx := context.Background()
|
||||
|
||||
// 创建测试角色
|
||||
role := &model.Role{
|
||||
RoleName: "rp_role",
|
||||
RoleDesc: "测试角色",
|
||||
RoleType: constants.RoleTypePlatform,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := roleStore.Create(ctx, role)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建测试权限
|
||||
permission := &model.Permission{
|
||||
PermName: "rp_permission",
|
||||
PermCode: "rp:permission",
|
||||
PermType: constants.PermissionTypeMenu,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err = permissionStore.Create(ctx, permission)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 创建关联
|
||||
rolePermission := &model.RolePermission{
|
||||
RoleID: role.ID,
|
||||
PermID: permission.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err = rolePermissionStore.Create(ctx, rolePermission)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("软删除角色-权限关联", func(t *testing.T) {
|
||||
err := rolePermissionStore.Delete(ctx, role.ID, permission.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 查询应该找不到
|
||||
permissions, err := rolePermissionStore.GetByRoleID(ctx, role.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, permissions, 0)
|
||||
})
|
||||
|
||||
t.Run("软删除后可以重新关联", func(t *testing.T) {
|
||||
newRolePermission := &model.RolePermission{
|
||||
RoleID: role.ID,
|
||||
PermID: permission.ID,
|
||||
Status: constants.StatusEnabled,
|
||||
}
|
||||
err := rolePermissionStore.Create(ctx, newRolePermission)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 验证可以查询到
|
||||
permissions, err := rolePermissionStore.GetByRoleID(ctx, role.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, permissions, 1)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user