完成错误测试用例
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# 后端 API 测试(TDD)
|
||||
|
||||
依据《产品文档》与《API 文档》,用 Python + pytest 编写。**先写失败用例(RED),再实现路由(GREEN)。**
|
||||
|
||||
后端源码在 `backend/src/` 下,pytest 通过 `pytest.ini` 的 `pythonpath = src` 自动加入 `src` 目录,无需手动设置 PYTHONPATH。
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
source .venv/bin/activate # 或 pip install -r requirements.txt
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
## 用例覆盖
|
||||
|
||||
| 接口 | 用例 |
|
||||
|------|------|
|
||||
| POST /api/auth/login | 成功返回 token/user、错误返回 401、缺 username/password 返回 400 |
|
||||
| POST /api/projects/list | 无 token 401、有 token 返回 list/total/page/pageSize、无数据时 list 空且 total=0 |
|
||||
| POST /api/projects/detail | 无 token 401、存在项目返回完整字段、不存在 404 |
|
||||
| POST /api/projects/create | 无 token 401、非市场部 403、市场部且必填齐全 201、缺合同编号/项目名称 400 |
|
||||
| POST /api/projects/update | 无 token 401、存在项目 200 保存成功、不存在 404 |
|
||||
| POST /api/projects/logs | 无 token 401、有 token 返回 list/total/page/pageSize、不存在项目 404 |
|
||||
@@ -0,0 +1,18 @@
|
||||
"""pytest 配置与公共 fixture(依据 API 文档)"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app import app, PROJECTS, OPERATION_LOGS
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_store():
|
||||
"""每个测试前清空内存存储,保证隔离"""
|
||||
PROJECTS.clear()
|
||||
OPERATION_LOGS.clear()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""同步测试用:FastAPI TestClient"""
|
||||
return TestClient(app)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
认证 API 测试(依据 API 文档 2.1 登录)
|
||||
TDD:先写失败用例,再实现路由
|
||||
"""
|
||||
|
||||
|
||||
def test_login_success_returns_200_with_token_and_user(client):
|
||||
"""正确账号密码返回 200 且包含 token 和 user"""
|
||||
r = client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "market", "password": "valid-password"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "token" in body
|
||||
assert "user" in body
|
||||
assert "id" in body["user"]
|
||||
assert "username" in body["user"]
|
||||
assert "role" in body["user"]
|
||||
assert "displayName" in body["user"]
|
||||
|
||||
|
||||
def test_login_wrong_credentials_returns_401_with_code_and_message(client):
|
||||
"""错误账号或密码返回 401 且包含 code 和 message"""
|
||||
r = client.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "nobody", "password": "wrong"},
|
||||
)
|
||||
assert r.status_code == 401
|
||||
body = r.json()
|
||||
assert body.get("code") == 401
|
||||
assert "message" in body
|
||||
|
||||
|
||||
def test_login_missing_username_returns_400(client):
|
||||
"""缺少 username 时返回 400"""
|
||||
r = client.post("/api/auth/login", json={"password": "any"})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_login_missing_password_returns_400(client):
|
||||
"""缺少 password 时返回 400"""
|
||||
r = client.post("/api/auth/login", json={"username": "any"})
|
||||
assert r.status_code == 400
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
项目 API 测试(依据 API 文档 3–7 节)
|
||||
TDD:先写失败用例,再实现路由
|
||||
"""
|
||||
|
||||
|
||||
def _auth_header(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
# ---------- POST /api/projects/list ----------
|
||||
|
||||
|
||||
def test_list_without_token_returns_401(client):
|
||||
"""项目列表无 token 时返回 401"""
|
||||
r = client.post("/api/projects/list", json={})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_list_with_token_returns_200_with_list_total_page_page_size(client):
|
||||
"""有 token 时返回 200 且包含 list、total、page、pageSize"""
|
||||
r = client.post(
|
||||
"/api/projects/list",
|
||||
headers=_auth_header("valid-token"),
|
||||
json={"page": 1, "pageSize": 20},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "list" in body
|
||||
assert isinstance(body["list"], list)
|
||||
assert "total" in body
|
||||
assert "page" in body
|
||||
assert "pageSize" in body
|
||||
|
||||
|
||||
def test_list_empty_result_has_empty_list_and_total_zero(client):
|
||||
"""无数据时 list 为空数组且 total 为 0"""
|
||||
r = client.post(
|
||||
"/api/projects/list",
|
||||
headers=_auth_header("valid-token"),
|
||||
json={},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["list"] == []
|
||||
assert r.json()["total"] == 0
|
||||
|
||||
|
||||
# ---------- POST /api/projects/detail ----------
|
||||
|
||||
|
||||
def test_detail_without_token_returns_401(client):
|
||||
"""项目详情无 token 时返回 401"""
|
||||
r = client.post("/api/projects/detail", json={"id": "any-id"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_detail_existing_project_returns_200_with_full_fields(client):
|
||||
"""存在项目时返回 200 且包含 id、contract、costControl、receivable、payable、other、createdAt、updatedAt"""
|
||||
create_r = client.post(
|
||||
"/api/projects/create",
|
||||
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"),
|
||||
json={"id": pid},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "id" in body
|
||||
assert "contract" in body
|
||||
assert "costControl" in body
|
||||
assert "receivable" in body
|
||||
assert "payable" in body
|
||||
assert "other" in body
|
||||
assert "createdAt" in body
|
||||
assert "updatedAt" in body
|
||||
|
||||
|
||||
def test_detail_nonexistent_project_returns_404_with_code_and_message(client):
|
||||
"""不存在项目时返回 404 且包含 code 和 message"""
|
||||
r = client.post(
|
||||
"/api/projects/detail",
|
||||
headers=_auth_header("valid-token"),
|
||||
json={"id": "non-existent-id"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
body = r.json()
|
||||
assert body.get("code") == 404
|
||||
assert "message" in body
|
||||
|
||||
|
||||
# ---------- POST /api/projects/create ----------
|
||||
|
||||
|
||||
def test_create_without_token_returns_401(client):
|
||||
"""新建项目无 token 时返回 401"""
|
||||
r = client.post(
|
||||
"/api/projects/create",
|
||||
json={"contract": {"contractCode": "HT001", "projectName": "测试项目"}},
|
||||
)
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_create_non_market_role_returns_403(client):
|
||||
"""非市场部角色返回 403"""
|
||||
r = client.post(
|
||||
"/api/projects/create",
|
||||
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):
|
||||
"""市场部且合同编号与项目名称齐全时返回 201 且包含 id 和 message"""
|
||||
r = client.post(
|
||||
"/api/projects/create",
|
||||
headers=_auth_header("market-token"),
|
||||
json={"contract": {"contractCode": "HT001", "projectName": "测试项目"}},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
body = r.json()
|
||||
assert "id" in body
|
||||
assert body.get("message") == "创建成功"
|
||||
|
||||
|
||||
def test_create_missing_contract_code_returns_400(client):
|
||||
"""缺少合同编号时返回 400"""
|
||||
r = client.post(
|
||||
"/api/projects/create",
|
||||
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):
|
||||
"""缺少项目名称时返回 400"""
|
||||
r = client.post(
|
||||
"/api/projects/create",
|
||||
headers=_auth_header("market-token"),
|
||||
json={"contract": {"contractCode": "HT001"}},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
assert r.json().get("code") == 400
|
||||
|
||||
|
||||
# ---------- POST /api/projects/update ----------
|
||||
|
||||
|
||||
def test_update_without_token_returns_401(client):
|
||||
"""更新项目无 token 时返回 401"""
|
||||
r = client.post("/api/projects/update", json={"id": "any-id"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_update_existing_project_returns_200_with_id_and_save_success_message(client):
|
||||
"""存在项目时返回 200 且包含 id 和 message「保存成功」"""
|
||||
create_r = client.post(
|
||||
"/api/projects/create",
|
||||
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"),
|
||||
json={"id": pid, "contract": {"projectName": "新名称"}},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "id" in body
|
||||
assert body.get("message") == "保存成功"
|
||||
|
||||
|
||||
def test_update_nonexistent_project_returns_404(client):
|
||||
"""不存在项目时返回 404"""
|
||||
r = client.post(
|
||||
"/api/projects/update",
|
||||
headers=_auth_header("valid-token"),
|
||||
json={"id": "non-existent-id"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
assert r.json().get("code") == 404
|
||||
|
||||
|
||||
# ---------- POST /api/projects/logs ----------
|
||||
|
||||
|
||||
def test_logs_without_token_returns_401(client):
|
||||
"""操作日志无 token 时返回 401"""
|
||||
r = client.post("/api/projects/logs", json={"id": "any-id"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_logs_with_token_returns_200_with_list_total_page_page_size(client):
|
||||
"""有 token 时返回 200 且包含 list、total、page、pageSize"""
|
||||
create_r = client.post(
|
||||
"/api/projects/create",
|
||||
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"),
|
||||
json={"id": pid},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "list" in body
|
||||
assert isinstance(body["list"], list)
|
||||
assert "total" in body
|
||||
assert "page" in body
|
||||
assert "pageSize" in body
|
||||
|
||||
|
||||
def test_logs_nonexistent_project_returns_404(client):
|
||||
"""不存在项目时返回 404"""
|
||||
r = client.post(
|
||||
"/api/projects/logs",
|
||||
headers=_auth_header("valid-token"),
|
||||
json={"id": "non-existent-id"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
assert r.json().get("code") == 404
|
||||
Reference in New Issue
Block a user