All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s
- 新增店铺角色管理 API 和数据模型 - 实现角色继承和权限检查逻辑 - 添加流程测试框架和集成测试 - 更新权限服务和账号管理逻辑 - 添加数据库迁移脚本 - 归档 OpenSpec 变更文档 Ultraworked with Sisyphus
648 lines
16 KiB
Markdown
648 lines
16 KiB
Markdown
# 流程测试规范文档
|
||
|
||
> 本文档定义了业务流程测试的编写规范。AI 助手根据用户描述的业务流程自动生成测试脚本。
|
||
|
||
## 快速开始
|
||
|
||
### 环境准备
|
||
|
||
```bash
|
||
cd flow_tests
|
||
python3 -m venv venv
|
||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
```
|
||
|
||
### 运行测试
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
pytest
|
||
|
||
# 运行指定模块
|
||
pytest tests/test_account_flow.py -v
|
||
|
||
# 运行指定流程
|
||
pytest tests/test_account_flow.py::test_create_agent_flow -v
|
||
|
||
# 查看详细输出
|
||
pytest -v --tb=short
|
||
```
|
||
|
||
---
|
||
|
||
## 用户如何描述流程
|
||
|
||
### 描述格式(自然语言即可)
|
||
|
||
用户只需用自然语言描述业务流程,AI 会自动:
|
||
1. 理解流程意图
|
||
2. 找到对应的后端接口
|
||
3. 生成测试脚本
|
||
4. 处理数据清理
|
||
|
||
### 描述示例
|
||
|
||
**示例 1:简单流程**
|
||
> "测试创建代理商:平台管理员创建一个店铺,然后给这个店铺创建管理员账号,验证这个账号能登录"
|
||
|
||
**示例 2:带条件的流程**
|
||
> "测试套餐分配:代理商只有被分配了套餐才能卖货。先创建店铺,不分配套餐时应该看不到任何可售套餐,分配后才能看到"
|
||
|
||
**示例 3:异步流程**
|
||
> "测试设备导入:上传 CSV 文件导入设备,这是异步任务,需要等待任务完成后验证设备确实入库了"
|
||
|
||
**示例 4:涉及第三方**
|
||
> "测试充值流程:用户下单充值,支付成功后卡片流量应该增加。支付回调可以直接模拟"
|
||
|
||
### 需要说明的要素
|
||
|
||
| 要素 | 必须 | 说明 |
|
||
|------|------|------|
|
||
| 操作角色 | 是 | 谁在操作(平台管理员/代理商/企业用户/普通用户) |
|
||
| 操作步骤 | 是 | 按顺序描述要做什么 |
|
||
| 预期结果 | 是 | 每步操作后应该发生什么 |
|
||
| 前置条件 | 否 | 如果有特殊前置条件需要说明 |
|
||
| 异步等待 | 否 | 如果涉及异步任务需要说明 |
|
||
|
||
---
|
||
|
||
## 测试框架结构
|
||
|
||
```
|
||
flow_tests/
|
||
├── AGENTS.md # 本规范文档
|
||
├── openapi.yaml # OpenAPI 接口文档(AI 助手必读)
|
||
├── requirements.txt # Python 依赖
|
||
├── pytest.ini # pytest 配置
|
||
├── config/
|
||
│ ├── settings.py # 配置加载
|
||
│ ├── local.yaml # 本地环境配置
|
||
│ └── remote.yaml # 远程环境配置
|
||
├── core/
|
||
│ ├── __init__.py
|
||
│ ├── client.py # HTTP 客户端封装
|
||
│ ├── auth.py # 认证管理
|
||
│ ├── database.py # 数据库直连(验证用)
|
||
│ ├── cleanup.py # 数据清理追踪器
|
||
│ ├── mock.py # 第三方服务 Mock
|
||
│ └── wait.py # 异步任务等待器
|
||
├── fixtures/
|
||
│ ├── __init__.py
|
||
│ └── common.py # 通用 pytest fixtures
|
||
└── tests/
|
||
├── __init__.py
|
||
├── test_account_flow.py # 账号管理流程
|
||
├── test_shop_flow.py # 店铺管理流程
|
||
└── ... # 其他模块流程
|
||
```
|
||
|
||
---
|
||
|
||
## 核心组件说明
|
||
|
||
### 1. HTTP 客户端 (core/client.py)
|
||
|
||
```python
|
||
from core.client import APIClient
|
||
|
||
# 创建客户端
|
||
client = APIClient()
|
||
|
||
# 登录获取 token
|
||
client.login("admin", "password")
|
||
|
||
# 发起请求(自动带 token)
|
||
resp = client.post("/api/admin/accounts", json={...})
|
||
resp = client.get("/api/admin/accounts/1")
|
||
|
||
# 断言响应
|
||
assert resp.ok() # code == 0
|
||
assert resp.code == 0
|
||
assert resp.data["id"] == 1
|
||
```
|
||
|
||
### 2. 数据清理追踪器 (core/cleanup.py)
|
||
|
||
**核心原则:只删除测试创建的数据,不影响原有数据**
|
||
|
||
```python
|
||
from core.cleanup import CleanupTracker
|
||
|
||
# 在 fixture 中初始化
|
||
tracker = CleanupTracker(db_connection)
|
||
|
||
# 记录创建的数据
|
||
account_id = create_account(...)
|
||
tracker.track("admin_accounts", account_id)
|
||
|
||
shop_id = create_shop(...)
|
||
tracker.track("shops", shop_id)
|
||
|
||
# 测试结束后自动清理(逆序删除,处理依赖)
|
||
tracker.cleanup() # 先删 account,再删 shop
|
||
```
|
||
|
||
### 3. 认证管理 (core/auth.py)
|
||
|
||
```python
|
||
from core.auth import AuthManager
|
||
|
||
auth = AuthManager(client)
|
||
|
||
# 预置角色快速登录
|
||
auth.as_super_admin() # 超级管理员
|
||
auth.as_platform_admin() # 平台管理员
|
||
auth.as_agent(shop_id) # 代理商(需指定店铺)
|
||
auth.as_enterprise(ent_id) # 企业用户
|
||
|
||
# 自定义账号登录
|
||
auth.login("custom_account", "password")
|
||
```
|
||
|
||
### 4. 异步任务等待器 (core/wait.py)
|
||
|
||
```python
|
||
from core.wait import wait_for_task, wait_for_condition
|
||
|
||
# 等待异步任务完成
|
||
result = wait_for_task(
|
||
task_type="device_import",
|
||
task_id=task_id,
|
||
timeout=60,
|
||
poll_interval=2
|
||
)
|
||
|
||
# 等待条件满足
|
||
wait_for_condition(
|
||
condition=lambda: db.query("SELECT count(*) FROM devices WHERE batch_id = %s", batch_id) > 0,
|
||
timeout=30,
|
||
message="等待设备入库"
|
||
)
|
||
```
|
||
|
||
### 5. Mock 服务 (core/mock.py)
|
||
|
||
```python
|
||
from core.mock import MockService
|
||
|
||
mock = MockService(db_connection)
|
||
|
||
# 模拟支付回调
|
||
mock.payment_success(order_id, amount=100)
|
||
|
||
# 模拟短信验证码(直接写入数据库/Redis)
|
||
mock.sms_code(phone="13800138000", code="123456")
|
||
|
||
# 模拟第三方 API 响应(如果后端支持 mock 模式)
|
||
mock.external_api("carrier_recharge", response={"success": True})
|
||
```
|
||
|
||
---
|
||
|
||
## 测试编写规范
|
||
|
||
### 基本结构
|
||
|
||
```python
|
||
"""
|
||
账号管理流程测试
|
||
|
||
测试场景:
|
||
1. 创建代理商账号流程
|
||
2. 账号权限验证流程
|
||
...
|
||
"""
|
||
import pytest
|
||
from core.client import APIClient
|
||
from core.auth import AuthManager
|
||
from core.cleanup import CleanupTracker
|
||
|
||
|
||
class TestAccountFlow:
|
||
"""账号管理流程"""
|
||
|
||
def test_create_agent_account_flow(self, client, auth, tracker, db):
|
||
"""
|
||
流程:创建代理商账号
|
||
|
||
步骤:
|
||
1. 平台管理员创建店铺
|
||
2. 给店铺创建管理员账号
|
||
3. 验证新账号能登录
|
||
4. 验证只能看到自己店铺的数据
|
||
"""
|
||
# === 1. 平台管理员创建店铺 ===
|
||
auth.as_platform_admin()
|
||
|
||
resp = client.post("/api/admin/shops", json={
|
||
"name": "测试代理商",
|
||
"contact": "张三",
|
||
"phone": "13800138000"
|
||
})
|
||
assert resp.ok(), f"创建店铺失败: {resp.msg}"
|
||
shop_id = resp.data["id"]
|
||
tracker.track("shops", shop_id)
|
||
|
||
# === 2. 创建店铺管理员账号 ===
|
||
resp = client.post("/api/admin/accounts", json={
|
||
"username": "test_agent_admin",
|
||
"password": "Test123456",
|
||
"shop_id": shop_id,
|
||
"role_ids": [2] # 假设 2 是店铺管理员角色
|
||
})
|
||
assert resp.ok(), f"创建账号失败: {resp.msg}"
|
||
account_id = resp.data["id"]
|
||
tracker.track("admin_accounts", account_id)
|
||
|
||
# === 3. 验证新账号能登录 ===
|
||
auth.login("test_agent_admin", "Test123456")
|
||
|
||
resp = client.get("/api/admin/auth/me")
|
||
assert resp.ok()
|
||
assert resp.data["shop_id"] == shop_id
|
||
|
||
# === 4. 验证只能看到自己店铺数据 ===
|
||
resp = client.get("/api/admin/shops")
|
||
assert resp.ok()
|
||
# 代理商只能看到自己的店铺
|
||
assert len(resp.data["list"]) == 1
|
||
assert resp.data["list"][0]["id"] == shop_id
|
||
```
|
||
|
||
### 命名规范
|
||
|
||
| 类型 | 规范 | 示例 |
|
||
|------|------|------|
|
||
| 文件名 | `test_{模块}_flow.py` | `test_account_flow.py` |
|
||
| 类名 | `Test{模块}Flow` | `TestAccountFlow` |
|
||
| 方法名 | `test_{流程描述}` | `test_create_agent_account_flow` |
|
||
| 方法文档 | 必须包含流程步骤 | 见上方示例 |
|
||
|
||
### Fixtures 使用
|
||
|
||
```python
|
||
# fixtures/common.py 提供以下通用 fixtures
|
||
|
||
@pytest.fixture
|
||
def client():
|
||
"""HTTP 客户端"""
|
||
return APIClient()
|
||
|
||
@pytest.fixture
|
||
def auth(client):
|
||
"""认证管理器"""
|
||
return AuthManager(client)
|
||
|
||
@pytest.fixture
|
||
def db():
|
||
"""数据库连接(只读验证用)"""
|
||
return get_db_connection()
|
||
|
||
@pytest.fixture
|
||
def tracker(db):
|
||
"""数据清理追踪器"""
|
||
t = CleanupTracker(db)
|
||
yield t
|
||
t.cleanup() # 测试结束自动清理
|
||
|
||
@pytest.fixture
|
||
def mock(db):
|
||
"""Mock 服务"""
|
||
return MockService(db)
|
||
```
|
||
|
||
---
|
||
|
||
## 异步任务测试规范
|
||
|
||
### 导入类任务(设备导入、IoT卡导入)
|
||
|
||
```python
|
||
def test_device_import_flow(self, client, auth, tracker, db):
|
||
"""
|
||
流程:设备批量导入
|
||
|
||
步骤:
|
||
1. 上传 CSV 文件
|
||
2. 创建导入任务
|
||
3. 等待任务完成
|
||
4. 验证设备入库
|
||
"""
|
||
auth.as_platform_admin()
|
||
|
||
# 1. 上传文件
|
||
with open("fixtures/devices.csv", "rb") as f:
|
||
resp = client.upload("/api/admin/storage/upload", file=f)
|
||
file_url = resp.data["url"]
|
||
|
||
# 2. 创建导入任务
|
||
resp = client.post("/api/admin/device-imports", json={
|
||
"file_url": file_url,
|
||
"carrier_id": 1
|
||
})
|
||
assert resp.ok()
|
||
task_id = resp.data["task_id"]
|
||
|
||
# 3. 等待任务完成
|
||
from core.wait import wait_for_task
|
||
result = wait_for_task("device_import", task_id, timeout=60)
|
||
assert result["status"] == "completed"
|
||
|
||
# 4. 验证设备入库
|
||
imported_count = db.scalar(
|
||
"SELECT count(*) FROM devices WHERE import_task_id = %s",
|
||
task_id
|
||
)
|
||
assert imported_count == 10 # CSV 中有 10 条
|
||
|
||
# 追踪清理
|
||
tracker.track_by_query("devices", f"import_task_id = {task_id}")
|
||
```
|
||
|
||
### 支付回调类任务
|
||
|
||
```python
|
||
def test_recharge_flow(self, client, auth, tracker, db, mock):
|
||
"""
|
||
流程:充值支付
|
||
|
||
步骤:
|
||
1. 用户创建充值订单
|
||
2. 模拟支付成功回调
|
||
3. 验证卡片流量增加
|
||
"""
|
||
auth.as_enterprise(enterprise_id=1)
|
||
|
||
# 1. 创建充值订单
|
||
resp = client.post("/api/h5/recharge/orders", json={
|
||
"card_id": 123,
|
||
"package_id": 456
|
||
})
|
||
assert resp.ok()
|
||
order_id = resp.data["order_id"]
|
||
tracker.track("orders", order_id)
|
||
|
||
# 获取支付前流量
|
||
before_data = db.scalar(
|
||
"SELECT data_balance FROM iot_cards WHERE id = 123"
|
||
)
|
||
|
||
# 2. 模拟支付回调
|
||
mock.payment_success(order_id, amount=50.00)
|
||
|
||
# 3. 验证流量增加
|
||
from core.wait import wait_for_condition
|
||
wait_for_condition(
|
||
condition=lambda: db.scalar(
|
||
"SELECT data_balance FROM iot_cards WHERE id = 123"
|
||
) > before_data,
|
||
timeout=10,
|
||
message="等待流量到账"
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 角色权限说明
|
||
|
||
测试时需要了解系统角色体系:
|
||
|
||
| 角色 | 说明 | 数据范围 |
|
||
|------|------|----------|
|
||
| 超级管理员 | 系统最高权限 | 全部数据 |
|
||
| 平台管理员 | 平台运营人员 | 全部数据(受权限配置限制) |
|
||
| 代理商管理员 | 店铺管理者 | 本店铺 + 下级店铺 |
|
||
| 代理商员工 | 店铺普通员工 | 本店铺 |
|
||
| 企业管理员 | 企业用户 | 本企业数据 |
|
||
|
||
---
|
||
|
||
## 环境配置
|
||
|
||
### config/local.yaml
|
||
|
||
```yaml
|
||
# 本地开发环境
|
||
api:
|
||
base_url: "http://localhost:3000"
|
||
timeout: 30
|
||
|
||
database:
|
||
host: "localhost"
|
||
port: 5432
|
||
name: "junhong_dev"
|
||
user: "postgres"
|
||
password: "postgres"
|
||
|
||
redis:
|
||
host: "localhost"
|
||
port: 6379
|
||
db: 0
|
||
|
||
# 预置测试账号
|
||
accounts:
|
||
super_admin:
|
||
username: "superadmin"
|
||
password: "Admin123456"
|
||
platform_admin:
|
||
username: "platform"
|
||
password: "Admin123456"
|
||
```
|
||
|
||
### config/remote.yaml
|
||
|
||
```yaml
|
||
# 远程测试环境
|
||
api:
|
||
base_url: "https://test-api.example.com"
|
||
timeout: 30
|
||
|
||
database:
|
||
host: "test-db.example.com"
|
||
port: 5432
|
||
name: "junhong_test"
|
||
user: "test_user"
|
||
password: "test_password"
|
||
|
||
redis:
|
||
host: "test-redis.example.com"
|
||
port: 6379
|
||
db: 0
|
||
```
|
||
|
||
### 切换环境
|
||
|
||
```bash
|
||
# 使用本地环境(默认)
|
||
pytest
|
||
|
||
# 使用远程环境
|
||
TEST_ENV=remote pytest
|
||
```
|
||
|
||
---
|
||
|
||
## 数据清理规则
|
||
|
||
### 清理原则
|
||
|
||
1. **只删除测试创建的数据** - 通过 tracker 追踪
|
||
2. **逆序删除** - 先删依赖方,再删被依赖方
|
||
3. **软删除优先** - 如果表支持软删除,使用软删除
|
||
4. **级联处理** - 自动处理关联数据
|
||
|
||
### 追踪方式
|
||
|
||
```python
|
||
# 方式1:追踪单条记录
|
||
tracker.track("table_name", record_id)
|
||
|
||
# 方式2:追踪多条记录
|
||
tracker.track_many("table_name", [id1, id2, id3])
|
||
|
||
# 方式3:按条件追踪(用于批量导入等场景)
|
||
tracker.track_by_query("devices", "import_task_id = 123")
|
||
|
||
# 方式4:追踪关联数据(自动级联)
|
||
tracker.track_with_relations("shops", shop_id, relations=[
|
||
("admin_accounts", "shop_id"),
|
||
("shop_packages", "shop_id")
|
||
])
|
||
```
|
||
|
||
### 清理顺序配置
|
||
|
||
```python
|
||
# core/cleanup.py 中定义表的依赖关系
|
||
TABLE_DEPENDENCIES = {
|
||
"admin_accounts": ["shops"], # accounts 依赖 shops
|
||
"shop_packages": ["shops", "packages"], # shop_packages 依赖 shops 和 packages
|
||
"orders": ["iot_cards", "packages"],
|
||
# ... 根据实际表结构配置
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
### Q: 如何处理需要真实第三方服务的测试?
|
||
|
||
A: 两种方式:
|
||
1. 使用 Mock 模式(推荐):直接模拟回调或写入预期数据
|
||
2. 如果必须真实调用:在配置中标记为 `skip_in_mock_mode`,仅在集成环境运行
|
||
|
||
### Q: 测试数据影响了其他人怎么办?
|
||
|
||
A:
|
||
1. 本地环境:数据隔离,不影响他人
|
||
2. 远程环境:使用唯一标识(如 UUID)创建数据,确保清理
|
||
|
||
### Q: 异步任务超时怎么办?
|
||
|
||
A:
|
||
1. 检查任务是否真的启动了
|
||
2. 检查 worker 是否在运行
|
||
3. 增加超时时间(最后手段)
|
||
4. 查看任务日志定位问题
|
||
|
||
---
|
||
|
||
## 接口文档(OpenAPI)
|
||
|
||
**重要**:生成测试时,AI 助手必须首先查阅 `flow_tests/openapi.yaml` 获取准确的接口信息。
|
||
|
||
### 文件位置
|
||
|
||
```
|
||
flow_tests/openapi.yaml # OpenAPI 3.0 规范文档
|
||
```
|
||
|
||
### 文档结构
|
||
|
||
```yaml
|
||
# openapi.yaml 结构
|
||
components:
|
||
schemas: # 数据模型定义(DTO)
|
||
DtoCreateShopRequest:
|
||
properties:
|
||
shop_name: { type: string }
|
||
shop_code: { type: string }
|
||
# ...
|
||
required: [shop_name, shop_code]
|
||
|
||
paths: # API 路径定义
|
||
/api/admin/shops:
|
||
post:
|
||
summary: 创建店铺
|
||
requestBody:
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/DtoCreateShopRequest'
|
||
responses:
|
||
'200':
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: '#/components/schemas/DtoShopResponse'
|
||
```
|
||
|
||
### AI 助手使用方式
|
||
|
||
1. **查找接口**:根据用户描述的流程,在 `paths` 中找到对应的 API 路径
|
||
2. **获取请求参数**:从 `requestBody.schema` 获取请求体结构
|
||
3. **获取响应格式**:从 `responses.200.schema` 获取响应体结构
|
||
4. **理解字段含义**:从 `components.schemas` 中查看字段的 `description`
|
||
|
||
### 示例:查找创建店铺接口
|
||
|
||
用户说:"创建一个店铺"
|
||
|
||
AI 助手应该:
|
||
1. 读取 `openapi.yaml`
|
||
2. 搜索 `shops` 相关路径 → 找到 `POST /api/admin/shops`
|
||
3. 查看 `DtoCreateShopRequest` 了解必填字段:`shop_name`, `shop_code`, `init_username`, `init_phone`, `init_password`
|
||
4. 生成正确的测试代码
|
||
|
||
```python
|
||
resp = client.post("/api/admin/shops", json={
|
||
"shop_name": "测试店铺",
|
||
"shop_code": "TEST001",
|
||
"init_username": "test_admin",
|
||
"init_phone": "13800138000",
|
||
"init_password": "Test123456",
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## AI 助手工作流程
|
||
|
||
当用户描述业务流程后,AI 助手按以下步骤工作:
|
||
|
||
1. **理解流程**:分析用户描述,提取操作角色、步骤、预期结果
|
||
2. **查阅接口文档**:读取 `flow_tests/openapi.yaml` 获取准确的接口路径、请求参数、响应格式
|
||
3. **生成测试**:按照本规范生成测试代码,使用 OpenAPI 文档中的字段定义
|
||
4. **补充清理**:添加数据追踪和清理逻辑
|
||
5. **运行验证**:执行测试确保通过
|
||
6. **报告结果**:告知用户测试结果,如发现问题则报告
|
||
|
||
### 接口查找优先级
|
||
|
||
| 优先级 | 来源 | 说明 |
|
||
|--------|------|------|
|
||
| 1 | `flow_tests/openapi.yaml` | **首选**,最准确的接口定义 |
|
||
| 2 | 项目代码 `internal/handler/` | OpenAPI 未覆盖时查找源码 |
|
||
| 3 | 项目代码 `internal/router/` | 确认路由注册 |
|
||
|
||
---
|
||
|
||
## 版本记录
|
||
|
||
| 版本 | 日期 | 说明 |
|
||
|------|------|------|
|
||
| 1.0 | 2026-02-02 | 初始版本 |
|
||
| 1.1 | 2026-02-02 | 增加 OpenAPI 接口文档规范 |
|