test: 预算接口契约测试 — 覆盖2026-08-25修复的4个契约缺口
1. plans返回dimension/unit字段(前端维度列/单位列依赖) 2. plans支持keyword搜索(前端搜索框依赖) 3. versions列表字段完整性(version/status/kpi_count/total_budget) 4. 批量分解幂等性(重复执行不产生重复记录) + 更新过时测试: versions端点从'预期404'改为'预期200'(API已恢复) 结果: 35 passed
This commit is contained in:
@@ -154,7 +154,7 @@ class TestBudgetVersions:
|
||||
BASE = "/api/cma/budget"
|
||||
|
||||
def test_create_and_submit_version(self, client: TestClient, db: Session):
|
||||
"""创建预算后查询版本并提交(端点已移除,预期404)"""
|
||||
"""创建预算后查询版本并提交(版本管理API 2026-08-25恢复)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_VER_KPI")
|
||||
@@ -166,33 +166,50 @@ class TestBudgetVersions:
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 10000.0},
|
||||
)
|
||||
|
||||
# 版本端点已移除
|
||||
# 版本列表应返回200(2026-08-25新增版本管理API)
|
||||
ver_resp = client.get(f"{self.BASE}/versions", headers=auth_header(token))
|
||||
assert ver_resp.status_code == 404, "versions端点已移除"
|
||||
assert ver_resp.status_code == 200, "versions端点应存在"
|
||||
versions = ver_resp.json()
|
||||
assert isinstance(versions, list)
|
||||
assert len(versions) >= 1
|
||||
|
||||
def test_approve_version(self, client: TestClient, db: Session):
|
||||
"""审批通过版本(端点已移除,预期404)"""
|
||||
"""审批通过版本"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_VER_APPROVE")
|
||||
client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 10000.0},
|
||||
)
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/versions/approve",
|
||||
headers=auth_header(token),
|
||||
json={"version": "v1.0", "action": "approved"},
|
||||
)
|
||||
assert resp.status_code == 404, "versions/approve端点已移除"
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "approved"
|
||||
|
||||
def test_reject_version(self, client: TestClient, db: Session):
|
||||
"""驳回版本(端点已移除,预期404)"""
|
||||
"""驳回版本"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="BUDGET_VER_REJECT")
|
||||
client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 10000.0},
|
||||
)
|
||||
|
||||
resp = client.post(
|
||||
f"{self.BASE}/versions/approve",
|
||||
headers=auth_header(token),
|
||||
json={"version": "v2.0", "action": "rejected"},
|
||||
json={"version": "v1.0", "action": "rejected"},
|
||||
)
|
||||
assert resp.status_code == 404, "versions/approve端点已移除"
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["status"] == "rejected"
|
||||
|
||||
|
||||
class TestBudgetDeviationReport:
|
||||
@@ -568,3 +585,101 @@ class TestBudgetPermissions:
|
||||
|
||||
resp = client.get(f"{self.BASE}/plans", headers=auth_header(token))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
class TestBudgetContract20260825:
|
||||
"""2026-08-25修复后的接口契约测试
|
||||
覆盖今天发现的前后端契约缺口:
|
||||
1. plans返回dimension/unit字段(前端表格依赖)
|
||||
2. plans支持keyword搜索(前端搜索框依赖)
|
||||
3. versions列表返回完整字段
|
||||
4. 批量分解幂等性(重复执行不产生重复记录)
|
||||
"""
|
||||
|
||||
BASE = "/api/cma/budget"
|
||||
|
||||
def test_plans_returns_dimension_and_unit(self, client: TestClient, db: Session):
|
||||
"""契约:plans必须返回dimension/unit字段(否则前端维度列/单位列空白)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="CONTRACT_DIM_KPI", dimension="customer", unit="%")
|
||||
|
||||
client.post(
|
||||
f"{self.BASE}/plans",
|
||||
headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 100.0},
|
||||
)
|
||||
|
||||
resp = client.get(f"{self.BASE}/plans", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
items = resp.json()["data"]
|
||||
assert len(items) >= 1
|
||||
# 关键:每条必须有dimension和unit
|
||||
for item in items:
|
||||
assert "dimension" in item, f"plans缺少dimension字段: {item}"
|
||||
assert "unit" in item, f"plans缺少unit字段: {item}"
|
||||
|
||||
def test_plans_keyword_search(self, client: TestClient, db: Session):
|
||||
"""契约:plans支持keyword按KPI名称模糊搜索(前端搜索框依赖)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi1 = create_test_kpi(db, kpi_code="CONTRACT_KW_AAA", kpi_name="营业收入AAA")
|
||||
kpi2 = create_test_kpi(db, kpi_code="CONTRACT_KW_BBB", kpi_name="毛利率BBB")
|
||||
|
||||
client.post(f"{self.BASE}/plans", headers=auth_header(token),
|
||||
json={"kpi_id": kpi1.id, "period": "2026-06", "budget_value": 100.0})
|
||||
client.post(f"{self.BASE}/plans", headers=auth_header(token),
|
||||
json={"kpi_id": kpi2.id, "period": "2026-06", "budget_value": 200.0})
|
||||
|
||||
resp = client.get(f"{self.BASE}/plans", headers=auth_header(token), params={"keyword": "营业收入AAA"})
|
||||
assert resp.status_code == 200
|
||||
items = resp.json()["data"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["kpi_code"] == "CONTRACT_KW_AAA"
|
||||
|
||||
def test_versions_fields(self, client: TestClient, db: Session):
|
||||
"""契约:versions列表返回version/status/kpi_count/total_budget字段"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="CONTRACT_VER_FIELDS")
|
||||
|
||||
client.post(f"{self.BASE}/plans", headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-06", "budget_value": 500.0})
|
||||
|
||||
resp = client.get(f"{self.BASE}/versions", headers=auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
versions = resp.json()
|
||||
assert len(versions) >= 1
|
||||
for v in versions:
|
||||
assert "version" in v
|
||||
assert "status" in v
|
||||
assert "kpi_count" in v
|
||||
assert "total_budget" in v
|
||||
|
||||
def test_batch_decompose_idempotent(self, client: TestClient, db: Session):
|
||||
"""契约:批量分解幂等 — 重复执行不产生重复记录(同KPI+期间+版本唯一)"""
|
||||
create_test_user(db)
|
||||
token = get_token_for_user(client)
|
||||
kpi = create_test_kpi(db, kpi_code="CONTRACT_DECOMP")
|
||||
|
||||
# 先创建年度预算(period=2026-00 或任意月份记录,让批量分解能聚合到)
|
||||
client.post(f"{self.BASE}/plans", headers=auth_header(token),
|
||||
json={"kpi_id": kpi.id, "period": "2026-01", "budget_value": 12000.0, "budget_year": 2026, "budget_month": 1})
|
||||
|
||||
# 第一次批量分解
|
||||
resp1 = client.post(f"{self.BASE}/auto-decompose", headers=auth_header(token),
|
||||
json={"year": 2026, "method": "equal"})
|
||||
assert resp1.status_code == 200
|
||||
|
||||
# 第二次批量分解(幂等:应该更新而非新增)
|
||||
resp2 = client.post(f"{self.BASE}/auto-decompose", headers=auth_header(token),
|
||||
json={"year": 2026, "method": "equal"})
|
||||
assert resp2.status_code == 200
|
||||
|
||||
# 检查无重复(同KPI+期间+版本)
|
||||
from sqlalchemy import func
|
||||
rows = db.query(BudgetPlan.kpi_id, BudgetPlan.period, BudgetPlan.version,
|
||||
func.count().label("cnt")).group_by(
|
||||
BudgetPlan.kpi_id, BudgetPlan.period, BudgetPlan.version).having(
|
||||
func.count() > 1).all()
|
||||
assert len(rows) == 0, f"存在重复预算记录: {rows}"
|
||||
|
||||
Reference in New Issue
Block a user