Files
junhong_cmp_fiber/tests/hurl/flows/card-price-verification-flow.hurl
huang 2ef8b8a705
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
fix: 修复分页字段/角色DTO/套餐详情/批量分配Bug,新增C端测试登录接口和Hurl价格验证测试
- 修复B-2:27个分页DTO字段名统一(list→items, page_size→size,删除total_pages)
- 修复B-1:角色接口统一返回DTO(id小写,消除GORM大写字段名)
- 修复C-1:套餐详情接口(GET /packages/:id)补充代理佣金字段
- 修复C-2:批量分配已存在记录由500改为静默跳过
- 修复C-3:创建系列授权响应在事务提交后构建,packages数组不再为空
- 新增C端开发测试登录接口(POST /api/c/v1/auth/dev-login,仅logging.development=true时生效)
- 新增Hurl测试:card-price-verification-flow.hurl(导入卡→分配→C端价格验证)
- 新增测试Excel数据文件和dev.env测试变量
2026-03-31 09:54:08 +08:00

382 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ============================================================
# 测试:卡导入 → 分配 → C 端价格验证
# 生成时间2026-03-30
# 涉及模块auth, iot_card(import), shop, package_series, package,
# shop_series_grant, shop_package_allocation, client_auth, client_asset
# ============================================================
# 流程:
# 1. 平台管理员登录
# 2. 查询运营商列表,获取 carrier_id
# 3. 获取对象存储上传 URLpurpose=iot_import
# 4. 上传 Excel 文件IoT 卡测试数据)到预签名 URL
# 5. 提交卡导入任务
# 6. 轮询导入任务直到完成status=3
# 7. 获取一级代理店铺 IDshop1_id
# 8. 获取二级代理店铺 IDshop2_id
# 9. 获取套餐系列 IDseries_id和套餐 IDpackage_id
# 10. 分配 ICCID_1 给一级代理shop1
# 11. 分配 ICCID_2 给二级代理shop2
# 12. 绑定两张卡到套餐系列
# 13. C 端测试登录ICCID_1
# 14. 验证 ICCID_1 的可购套餐 → retail_price = 一级代理零售价15000
# 15. C 端测试登录ICCID_2
# 16. 验证 ICCID_2 的可购套餐 → retail_price = 二级代理零售价18000
# ============================================================
# 前置条件:
# 1. 已运行 package-resource-full-flow.hurl创建了 shop1/shop2/series/package
# 2. 开启 logging.development=true服务配置
# 3. 已配置对象存储S3/MinIO步骤 3-4 依赖对象存储上传
# 4. env 文件中已配置 agent1_username/agent2_username
# ============================================================
# ══════════════════════════════════════════════════════════════
# 第 1 步:平台管理员登录
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/auth/login
Content-Type: application/json
{
"username": "{{admin_username}}",
"password": "{{admin_password}}",
"device": "web"
}
HTTP 200
[Captures]
admin_token: jsonpath "$.data.access_token"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.access_token" isString
jsonpath "$.data.user.user_type" == 1
# ══════════════════════════════════════════════════════════════
# 第 2 步:查询运营商列表,获取第一个可用 carrier_id
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/admin/carriers?page=1&page_size=1&status=1
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
carrier_id: jsonpath "$.data.items[0].id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.items" count >= 1
jsonpath "$.data.items[0].id" isInteger
# ══════════════════════════════════════════════════════════════
# 第 3 步:获取对象存储上传 URLIoT 卡导入 Excel
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/admin/storage/upload-url
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"file_name": "hurl-test-cards.xlsx",
"content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"purpose": "iot_import"
}
HTTP 200
[Captures]
upload_url: jsonpath "$.data.upload_url"
file_key: jsonpath "$.data.file_key"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.upload_url" isString
jsonpath "$.data.file_key" isString
jsonpath "$.data.expires_in" isInteger
# ══════════════════════════════════════════════════════════════
# 第 4 步:上传 Excel 文件到预签名 URL
# 文件路径相对于 Hurl 运行目录tests/hurl/
# ══════════════════════════════════════════════════════════════
PUT {{upload_url}}
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
file,testdata/hurl-test-cards.xlsx;
HTTP 200
# ══════════════════════════════════════════════════════════════
# 第 5 步:提交 IoT 卡导入任务
# 导入 Excel 包含 ICCID_1HURL-TEST-CARD-001和 ICCID_2HURL-TEST-CARD-002
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/admin/iot-cards/import
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"carrier_id": {{carrier_id}},
"batch_no": "HURL-PRICE-TEST",
"file_key": "{{file_key}}"
}
HTTP 200
[Captures]
import_task_id: jsonpath "$.data.task_id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.task_id" isInteger
jsonpath "$.data.task_no" isString
# ══════════════════════════════════════════════════════════════
# 第 6 步轮询导入任务等待完成status=3 表示已完成)
# 注Hurl 不支持循环轮询,这里做 3 次间隔查询覆盖大多数场景
# 如需更长等待,可手动重跑此步骤
# ══════════════════════════════════════════════════════════════
# 第 1 次查询(立即)
GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}}
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.id" == {{import_task_id}}
jsonpath "$.data.status" isInteger
# 等待 2 秒后第 2 次查询Hurl 通过 delay 选项实现)
GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}}
Authorization: Bearer {{admin_token}}
[Options]
delay: 2000
HTTP 200
[Asserts]
jsonpath "$.code" == 0
# 等待 2 秒后第 3 次查询,断言任务成功完成
GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}}
Authorization: Bearer {{admin_token}}
[Options]
delay: 2000
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.status" == 3
jsonpath "$.data.success_count" >= 1
# ══════════════════════════════════════════════════════════════
# 第 7 步:获取一级代理店铺 IDshop1_id
# 依赖 package-resource-full-flow.hurl 已创建 agent1_username 账号
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/admin/accounts?username={{agent1_username}}&page=1&page_size=1
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
shop1_id: jsonpath "$.data.items[0].shop_id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.items" count >= 1
jsonpath "$.data.items[0].username" == "{{agent1_username}}"
jsonpath "$.data.items[0].user_type" == 3
jsonpath "$.data.items[0].shop_id" isInteger
# ══════════════════════════════════════════════════════════════
# 第 8 步:获取二级代理店铺 IDshop2_id
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/admin/accounts?username={{agent2_username}}&page=1&page_size=1
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
shop2_id: jsonpath "$.data.items[0].shop_id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.items" count >= 1
jsonpath "$.data.items[0].username" == "{{agent2_username}}"
jsonpath "$.data.items[0].user_type" == 3
jsonpath "$.data.items[0].shop_id" isInteger
# ══════════════════════════════════════════════════════════════
# 第 9 步:获取套餐系列 ID 和套餐 ID
# 依赖 package-resource-full-flow.hurl 创建的 "Hurl测试系列"
# ══════════════════════════════════════════════════════════════
# 获取套餐系列(按名称模糊匹配,取最新创建的)
GET {{base_url}}/api/admin/package-series?series_name=Hurl%E6%B5%8B%E8%AF%95%E7%B3%BB%E5%88%97&page=1&page_size=1
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
series_id: jsonpath "$.data.items[0].id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.items" count >= 1
jsonpath "$.data.items[0].series_name" == "Hurl测试系列"
# 获取该系列下的套餐(取最新的)
GET {{base_url}}/api/admin/packages?series_id={{series_id}}&page=1&page_size=1
Authorization: Bearer {{admin_token}}
HTTP 200
[Captures]
package_id: jsonpath "$.data.items[0].id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.items" count >= 1
jsonpath "$.data.items[0].series_id" == {{series_id}}
# ══════════════════════════════════════════════════════════════
# 第 10 步:分配 ICCID_18986001234560001给一级代理shop1
# 幂等处理HTTP * 兼容已分配场景
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/admin/iot-cards/standalone/allocate
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"to_shop_id": {{shop1_id}},
"selection_type": "list",
"iccids": ["{{test_iccid_1}}"],
"remark": "Hurl 价格验证测试 - 一级代理"
}
HTTP *
[Asserts]
jsonpath "$.code" isInteger
# ══════════════════════════════════════════════════════════════
# 第 11 步:分配 ICCID_28986001234560002给二级代理shop2
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/admin/iot-cards/standalone/allocate
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"to_shop_id": {{shop2_id}},
"selection_type": "list",
"iccids": ["{{test_iccid_2}}"],
"remark": "Hurl 价格验证测试 - 二级代理"
}
HTTP *
[Asserts]
jsonpath "$.code" isInteger
# ══════════════════════════════════════════════════════════════
# 第 12 步绑定两张卡到套餐系列series_binding
# 卡需要绑定套餐系列才能在 C 端看到可购套餐
# ══════════════════════════════════════════════════════════════
PATCH {{base_url}}/api/admin/iot-cards/series-binding
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{
"iccids": ["{{test_iccid_1}}", "{{test_iccid_2}}"],
"series_id": {{series_id}}
}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.success_count" >= 1
# ══════════════════════════════════════════════════════════════
# 第 13 步C 端测试登录ICCID_1归属一级代理
# 调用开发环境专用接口,传入 ICCID 自动绑定测试客户并签发 JWT
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/c/v1/auth/dev-login
Content-Type: application/json
{
"identifier": "{{test_iccid_1}}"
}
HTTP 200
[Captures]
customer1_token: jsonpath "$.data.token"
customer1_id: jsonpath "$.data.customer_id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.token" isString
jsonpath "$.data.customer_id" isInteger
# ══════════════════════════════════════════════════════════════
# 第 14 步:验证 ICCID_1 的可购套餐价格 = 一级代理零售价15000 分)
# retail_price 来自一级代理在 shop_package_allocation 中设置的零售价
# 前置条件package-resource-full-flow.hurl 中一级代理已设置零售价 15000
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_1}}
Authorization: Bearer {{customer1_token}}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.packages" isList
jsonpath "$.data.packages" count >= 1
# 关键断言一级代理归属的卡C 端看到的是一级代理设置的零售价 15000
jsonpath "$.data.packages[0].retail_price" == 15000
jsonpath "$.data.packages[0].package_id" == {{package_id}}
# ══════════════════════════════════════════════════════════════
# 第 15 步C 端测试登录ICCID_2归属二级代理
# ══════════════════════════════════════════════════════════════
POST {{base_url}}/api/c/v1/auth/dev-login
Content-Type: application/json
{
"identifier": "{{test_iccid_2}}"
}
HTTP 200
[Captures]
customer2_token: jsonpath "$.data.token"
customer2_id: jsonpath "$.data.customer_id"
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.token" isString
jsonpath "$.data.customer_id" isInteger
# ══════════════════════════════════════════════════════════════
# 第 16 步:验证 ICCID_2 的可购套餐价格 = 二级代理零售价18000 分)
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_2}}
Authorization: Bearer {{customer2_token}}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.packages" isList
jsonpath "$.data.packages" count >= 1
# 关键断言二级代理归属的卡C 端看到的是二级代理设置的零售价 18000与一级代理不同
jsonpath "$.data.packages[0].retail_price" == 18000
jsonpath "$.data.packages[0].package_id" == {{package_id}}
# ══════════════════════════════════════════════════════════════
# 第 17 步:用两个 token 访问对方的卡(越权验证)
# customer1 不应能查询 ICCID_2 的套餐(未绑定该资产)
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_2}}
Authorization: Bearer {{customer1_token}}
HTTP 403
[Asserts]
jsonpath "$.code" == 1005
# ══════════════════════════════════════════════════════════════
# 第 18 步:验证同一套餐两个代理视角价格确实不同(一致性检查)
# ══════════════════════════════════════════════════════════════
GET {{base_url}}/api/c/v1/asset/info?identifier={{test_iccid_1}}
Authorization: Bearer {{customer1_token}}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.asset_type" == "card"
jsonpath "$.data.iccid" == "{{test_iccid_1}}"
GET {{base_url}}/api/c/v1/asset/info?identifier={{test_iccid_2}}
Authorization: Bearer {{customer2_token}}
HTTP 200
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.data.asset_type" == "card"
jsonpath "$.data.iccid" == "{{test_iccid_2}}"