From 046a7cf8b9614780bd370a2135458c958020e6c9 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Tue, 25 Aug 2026 18:22:04 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E9=A2=84=E7=AE=97=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E6=B5=8B=E8=AF=95=20=E2=80=94=20=E8=A6=86?= =?UTF-8?q?=E7=9B=962026-08-25=E4=BF=AE=E5=A4=8D=E7=9A=844=E4=B8=AA?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E7=BC=BA=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. plans返回dimension/unit字段(前端维度列/单位列依赖) 2. plans支持keyword搜索(前端搜索框依赖) 3. versions列表字段完整性(version/status/kpi_count/total_budget) 4. 批量分解幂等性(重复执行不产生重复记录) + 更新过时测试: versions端点从'预期404'改为'预期200'(API已恢复) 结果: 35 passed --- backend/tests/test_budget.py | 131 ++++++++++++++++++++++++++++++++--- 1 file changed, 123 insertions(+), 8 deletions(-) diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index b94c62d3..a6ce1d3a 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -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}"