后端第一步开发完成,第一版回归测试完成

This commit is contained in:
Your Name
2026-01-31 19:09:48 +08:00
parent 11cef531bd
commit c9e75030af
26 changed files with 1686 additions and 184 deletions
+263 -31
View File
@@ -1,6 +1,8 @@
"""
项目 API 测试(依据 API 文档 3–7 节)
TDD:先写失败用例,再实现路由
测试与开发环境严格隔离:通过 HTTP 请求已启动的后端服务,带 Token 调用项目列表/详情/新建/更新/操作日志。
运行前请先启动后端:http://127.0.0.1:8000;登录账号见 conftestadmin / market / engineer 等,密码 123456)。
"""
@@ -17,11 +19,11 @@ def test_list_without_token_returns_401(client):
assert r.status_code == 401
def test_list_with_token_returns_200_with_list_total_page_page_size(client):
def test_list_with_token_returns_200_with_list_total_page_page_size(client, auth_token):
"""有 token 时返回 200 且包含 list、total、page、pageSize"""
r = client.post(
"/api/projects/list",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"page": 1, "pageSize": 20},
)
assert r.status_code == 200
@@ -33,16 +35,139 @@ def test_list_with_token_returns_200_with_list_total_page_page_size(client):
assert "pageSize" in body
def test_list_empty_result_has_empty_list_and_total_zero(client):
"""无数据时 list 为空数组且 total 为 0"""
def test_list_empty_result_has_empty_list_and_total_zero(client, auth_token):
"""列表返回 200无数据时 list 为空且 total 为 0(有数据时仅校验结构)"""
r = client.post(
"/api/projects/list",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={},
)
assert r.status_code == 200
assert r.json()["list"] == []
assert r.json()["total"] == 0
body = r.json()
assert "list" in body and isinstance(body["list"], list)
assert "total" in body and isinstance(body["total"], int)
if len(body["list"]) == 0:
assert body["total"] == 0
def test_list_accepts_search_type_name_and_keyword(client, auth_token):
"""支持 searchType=name 与 keyword 搜索项目名称"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"searchType": "name", "keyword": "测试"},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert "total" in body
def test_list_accepts_search_type_code_and_keyword(client, auth_token):
"""支持 searchType=code 与 keyword 搜索项目编号"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"searchType": "code", "keyword": "HT001"},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert "total" in body
def test_list_accepts_progress_filter(client, auth_token):
"""支持按 progress 筛选项目进度"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"progress": "进行中"},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert "total" in body
def test_list_accepts_cost_filter(client, auth_token):
"""支持按 cost 筛选项目费用"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"cost": "100万以下"},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert "total" in body
def test_list_accepts_date_filter_and_date_range(client, auth_token):
"""支持 dateFilterType 与 dateFrom、dateTo 时间筛选"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={
"dateFilterType": "signDate",
"dateFrom": "2024-01-01",
"dateTo": "2024-12-31",
},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert "total" in body
def test_list_accepts_date_abnormal_filter(client, auth_token):
"""支持 dateAbnormal=true 仅返回日期异常项目"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"dateAbnormal": True},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert isinstance(body["list"], list)
assert "total" in body
def test_list_items_contain_required_fields(client, auth_token, market_token):
"""列表项包含 id、projectName、contractCode、progress、cost、updatedAt"""
client.post(
"/api/projects/create",
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT-LIST", "projectName": "列表项字段测试"}},
)
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"page": 1, "pageSize": 20},
)
assert r.status_code == 200
body = r.json()
assert len(body["list"]) >= 1
item = body["list"][0]
assert "id" in item
assert "projectName" in item
assert "contractCode" in item
assert "progress" in item
assert "cost" in item
assert "updatedAt" in item
def test_list_respects_page_and_page_size(client, auth_token):
"""请求 page、pageSize 时响应中 page、pageSize 与请求一致"""
r = client.post(
"/api/projects/list",
headers=_auth_header(auth_token),
json={"page": 2, "pageSize": 5},
)
assert r.status_code == 200
body = r.json()
assert body.get("page") == 2
assert body.get("pageSize") == 5
# ---------- POST /api/projects/detail ----------
@@ -54,18 +179,18 @@ def test_detail_without_token_returns_401(client):
assert r.status_code == 401
def test_detail_existing_project_returns_200_with_full_fields(client):
def test_detail_existing_project_returns_200_with_full_fields(client, auth_token, market_token):
"""存在项目时返回 200 且包含 id、contract、costControl、receivable、payable、other、createdAt、updatedAt"""
create_r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT001", "projectName": "测试项目"}},
)
assert create_r.status_code == 201
pid = create_r.json()["id"]
r = client.post(
"/api/projects/detail",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": pid},
)
assert r.status_code == 200
@@ -80,11 +205,11 @@ def test_detail_existing_project_returns_200_with_full_fields(client):
assert "updatedAt" in body
def test_detail_nonexistent_project_returns_404_with_code_and_message(client):
def test_detail_nonexistent_project_returns_404_with_code_and_message(client, auth_token):
"""不存在项目时返回 404 且包含 code 和 message"""
r = client.post(
"/api/projects/detail",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": "non-existent-id"},
)
assert r.status_code == 404
@@ -93,6 +218,18 @@ def test_detail_nonexistent_project_returns_404_with_code_and_message(client):
assert "message" in body
def test_detail_missing_id_returns_400(client, auth_token):
"""缺少 id 时返回 400 或 422(参数错误)"""
r = client.post(
"/api/projects/detail",
headers=_auth_header(auth_token),
json={},
)
assert r.status_code in (400, 422)
body = r.json()
assert body.get("code") in (400, 422) or "detail" in body
# ---------- POST /api/projects/create ----------
@@ -105,22 +242,22 @@ def test_create_without_token_returns_401(client):
assert r.status_code == 401
def test_create_non_market_role_returns_403(client):
def test_create_non_market_role_returns_403(client, engineer_token):
"""非市场部角色返回 403"""
r = client.post(
"/api/projects/create",
headers=_auth_header("engineer-token"),
headers=_auth_header(engineer_token),
json={"contract": {"contractCode": "HT001", "projectName": "测试项目"}},
)
assert r.status_code == 403
assert r.json().get("code") == 403
def test_create_market_with_contract_code_and_name_returns_201_with_id_and_message(client):
def test_create_market_with_contract_code_and_name_returns_201_with_id_and_message(client, market_token):
"""市场部且合同编号与项目名称齐全时返回 201 且包含 id 和 message"""
r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT001", "projectName": "测试项目"}},
)
assert r.status_code == 201
@@ -129,28 +266,43 @@ def test_create_market_with_contract_code_and_name_returns_201_with_id_and_messa
assert body.get("message") == "创建成功"
def test_create_missing_contract_code_returns_400(client):
def test_create_missing_contract_code_returns_400(client, market_token):
"""缺少合同编号时返回 400"""
r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"projectName": "测试项目"}},
)
assert r.status_code == 400
assert r.json().get("code") == 400
def test_create_missing_project_name_returns_400(client):
def test_create_missing_project_name_returns_400(client, market_token):
"""缺少项目名称时返回 400"""
r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT001"}},
)
assert r.status_code == 400
assert r.json().get("code") == 400
def test_create_validation_error_may_include_errors_array(client, market_token):
"""参数校验失败时响应可包含 errors 数组(field、message"""
r = client.post(
"/api/projects/create",
headers=_auth_header(market_token),
json={"contract": {}},
)
assert r.status_code == 400
body = r.json()
assert body.get("code") == 400
if "errors" in body and body["errors"]:
for e in body["errors"]:
assert "field" in e or "message" in e
# ---------- POST /api/projects/update ----------
@@ -160,18 +312,30 @@ def test_update_without_token_returns_401(client):
assert r.status_code == 401
def test_update_existing_project_returns_200_with_id_and_save_success_message(client):
def test_update_missing_id_returns_400(client, auth_token):
"""更新项目缺少 id 时返回 400 或 422(参数错误)"""
r = client.post(
"/api/projects/update",
headers=_auth_header(auth_token),
json={"contract": {"projectName": "任意"}},
)
assert r.status_code in (400, 422)
body = r.json()
assert body.get("code") in (400, 422) or "detail" in body
def test_update_existing_project_returns_200_with_id_and_save_success_message(client, auth_token, market_token):
"""存在项目时返回 200 且包含 id 和 message「保存成功」"""
create_r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT002", "projectName": "测试项目2"}},
)
assert create_r.status_code == 201
pid = create_r.json()["id"]
r = client.post(
"/api/projects/update",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": pid, "contract": {"projectName": "新名称"}},
)
assert r.status_code == 200
@@ -180,11 +344,11 @@ def test_update_existing_project_returns_200_with_id_and_save_success_message(cl
assert body.get("message") == "保存成功"
def test_update_nonexistent_project_returns_404(client):
def test_update_nonexistent_project_returns_404(client, auth_token):
"""不存在项目时返回 404"""
r = client.post(
"/api/projects/update",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": "non-existent-id"},
)
assert r.status_code == 404
@@ -200,18 +364,30 @@ def test_logs_without_token_returns_401(client):
assert r.status_code == 401
def test_logs_with_token_returns_200_with_list_total_page_page_size(client):
def test_logs_missing_id_returns_400(client, auth_token):
"""操作日志缺少 id 时返回 400 或 422(参数错误)"""
r = client.post(
"/api/projects/logs",
headers=_auth_header(auth_token),
json={},
)
assert r.status_code in (400, 422)
body = r.json()
assert body.get("code") in (400, 422) or "detail" in body
def test_logs_with_token_returns_200_with_list_total_page_page_size(client, auth_token, market_token):
"""有 token 时返回 200 且包含 list、total、page、pageSize"""
create_r = client.post(
"/api/projects/create",
headers=_auth_header("market-token"),
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT003", "projectName": "测试项目3"}},
)
assert create_r.status_code == 201
pid = create_r.json()["id"]
r = client.post(
"/api/projects/logs",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": pid},
)
assert r.status_code == 200
@@ -223,12 +399,68 @@ def test_logs_with_token_returns_200_with_list_total_page_page_size(client):
assert "pageSize" in body
def test_logs_nonexistent_project_returns_404(client):
def test_logs_nonexistent_project_returns_404(client, auth_token):
"""不存在项目时返回 404"""
r = client.post(
"/api/projects/logs",
headers=_auth_header("valid-token"),
headers=_auth_header(auth_token),
json={"id": "non-existent-id"},
)
assert r.status_code == 404
assert r.json().get("code") == 404
def test_logs_after_update_returns_record_with_summary(client, auth_token, market_token):
"""更新项目后调用操作日志接口,至少返回一条记录且含 summary"""
create_r = client.post(
"/api/projects/create",
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT-LOG", "projectName": "日志测试"}},
)
assert create_r.status_code == 201
pid = create_r.json()["id"]
client.post(
"/api/projects/update",
headers=_auth_header(auth_token),
json={"id": pid, "contract": {"projectName": "日志测试已更新"}},
)
r = client.post(
"/api/projects/logs",
headers=_auth_header(auth_token),
json={"id": pid},
)
assert r.status_code == 200
body = r.json()
assert "list" in body
assert len(body["list"]) >= 1
first = body["list"][0]
assert "summary" in first
def test_logs_list_items_contain_required_fields(client, auth_token, market_token):
"""操作日志列表项包含 id、operatorId、operatorName、operatedAt、summary"""
create_r = client.post(
"/api/projects/create",
headers=_auth_header(market_token),
json={"contract": {"contractCode": "HT-LOG2", "projectName": "日志字段测试"}},
)
pid = create_r.json()["id"]
client.post(
"/api/projects/update",
headers=_auth_header(auth_token),
json={"id": pid, "contract": {"projectName": "更新"}},
)
r = client.post(
"/api/projects/logs",
headers=_auth_header(auth_token),
json={"id": pid},
)
assert r.status_code == 200
body = r.json()
assert len(body["list"]) >= 1
item = body["list"][0]
assert "id" in item
assert "operatorId" in item
assert "operatorName" in item
assert "operatedAt" in item
assert "summary" in item