"""波次1-2 修复行为聚焦验证(TestClient,无需起服务)。 运行: cd d:\dpb\dpb\backend C:\ai\miniconda3\envs\dpb\python.exe -X utf8 scripts/test_bre_wave12_tc.py 覆盖: 1) 1.1 模糊 LIKE 搜索(site_name 部分匹配 / personnel.name / trait_name) 2) 2.1 create 前置校验: 引用不存在的父记录 -> 明确 409(非 DB 原文) 3) 2.5 跨基地迁移: 空 plot 换基地成功; 换到不存在基地 -> 409; (site_id,plot_code) 重码 -> 409 4) 2.2 delete 下游校验: 删有 plot 的 site -> 409; 删有 tree 的 plot -> 409 5) 2.3 唯一约束: 同 site 下重复 plot_code -> 409 """ import os os.environ["ENVIRONMENT"] = "dev" os.environ["PYTHONUTF8"] = "1" import sys sys.path.insert(0, r"d:\dpb\dpb\backend") import main from fastapi.testclient import TestClient create_app = main.create_app TOKEN = None def login(client): global TOKEN d = {"username": "super", "password": "123456", "grant_type": "password", "login_type": "PC端"} r = client.post("/api/v1/system/auth/login", data=d) b = r.json() if r.status_code == 200 and b.get("code") == 0: TOKEN = b["data"]["access_token"]; return key = client.get("/api/v1/system/auth/captcha/get").json()["data"]["key"] client.post("/api/v1/system/auth/captcha/slider/complete", json={"captcha_key": key}) d["captcha_key"] = key r = client.post("/api/v1/system/auth/login", data=d) b = r.json() assert r.status_code == 200 and b.get("code") == 0, f"LOGIN FAIL {r.status_code} {b}" TOKEN = b["data"]["access_token"] def auth(): return {"Authorization": f"Bearer {TOKEN}"} def call(client, method, url, **kw): r = client.request(method, url, headers=auth(), **kw) try: body = r.json() except Exception: body = {"_raw": r.text[:300]} return r.status_code, body def main_(): with TestClient(create_app()) as client: login(client) RUN = __import__("datetime").datetime.now().strftime("%H%M%S") passed = 0 def expect(name, cond, detail=""): nonlocal passed ok = bool(cond) print(f"[{'OK ' if ok else 'FAIL'}] {name}" + (f" ({detail})" if detail and not ok else "")) if not ok: sys.exit(1) passed += 1 print("\n=== 1) 模糊 LIKE 搜索 ===") kw = f"模糊{RUN}" st = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": kw + "A"}) expect("create site A", st[0] == 200, st[1]) st2 = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": "普通" + RUN}) expect("create site B", st2[0] == 200, st2[1]) r = call(client, "GET", "/api/v1/bre/site/list", params={"page": 1, "page_size": 20, "site_name": kw}) expect("site_name LIKE '%kw%' 返回匹配", r[0] == 200 and any(kw in s.get("site_name", "") for s in r[1]["data"]["items"]), r[1]) r2 = call(client, "GET", "/api/v1/bre/site/list", params={"page": 1, "page_size": 20, "site_name": "不存在的词_xyz_不存在"}) items = r2[1]["data"]["items"] if r2[0] == 200 else [] expect("模糊词无结果返回空", r2[0] == 200 and items == [], r2[1]) per = call(client, "POST", "/api/v1/bre/personnel/create", json={"name": f"张{RUN}"}) expect("create personnel", per[0] == 200, per[1]) r3 = call(client, "GET", "/api/v1/bre/personnel/list", params={"page": 1, "page_size": 20, "name": RUN}) expect("personnel.name 模糊匹配", r3[0] == 200 and len(r3[1]["data"]["items"]) == 1, r3[1]) print("\n=== 2) 2.1 前置校验: 缺失父记录 -> 明确 409 ===") r = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": 99999999, "plot_code": f"X{RUN}"}) expect("plot.create 不存在 site_id -> 409", r[0] == 409 and ("基地" in r[1].get("msg", "") or "不存在" in r[1].get("msg", "")), r[1]) r = call(client, "POST", "/api/v1/bre/tree/create", json={"combination_id": 99999999, "tree_no": f"T{RUN}", "status": "alive"}) expect("tree.create 不存在 combination_id -> 409", r[0] == 409 and "组合" in r[1].get("msg", ""), r[1]) r = call(client, "POST", "/api/v1/bre/selection_rule/create", json={"target_id": 99999999, "rule_name": f"R{RUN}", "conditions_json": "{}"}) expect("selection_rule.create 不存在 target_id -> 409", r[0] == 409 and "目标" in r[1].get("msg", ""), r[1]) r = call(client, "POST", "/api/v1/bre/trial_study/create", json={"trial_id": 99999999, "study_name": f"S{RUN}"}) expect("trial_study.create 不存在 trial_id -> 409", r[0] == 409 and "试验" in r[1].get("msg", ""), r[1]) r = call(client, "POST", "/api/v1/bre/pedigree/create", json={"combination_id": 99999999, "child_code": f"C{RUN}"}) expect("pedigree.create 不存在 combination_id -> 409", r[0] == 409, r[1]) print("\n=== 3) 2.5 跨基地迁移 + 2.3 唯一约束 ===") siteA = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": f"迁移A{RUN}"})[1]["data"] siteB = call(client, "POST", "/api/v1/bre/site/create", json={"site_name": f"迁移B{RUN}"})[1]["data"] plot = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"P{RUN}"}) expect("plot.create", plot[0] == 200, plot[1]) plot_id = plot[1]["data"]["id"] r = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"P{RUN}"}) expect("同基地重码 plot_code -> 409", r[0] == 409, r[1]) r = call(client, "PUT", f"/api/v1/bre/plot/update/{plot_id}", json={"site_id": 88888888, "plot_code": f"P{RUN}"}) expect("plot 换到不存在基地 -> 409", r[0] == 409, r[1]) r = call(client, "PUT", f"/api/v1/bre/plot/update/{plot_id}", json={"site_id": siteB["id"], "plot_code": f"P{RUN}"}) expect("空 plot 换基地成功", r[0] == 200, r[1]) print("\n=== 4) 2.2 delete 下游校验 ===") plot2 = call(client, "POST", "/api/v1/bre/plot/create", json={"site_id": siteA["id"], "plot_code": f"Q{RUN}"}) expect("plot2.create", plot2[0] == 200, plot2[1]) r = call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteA["id"]]) expect("删有 plot 的 site -> 409", r[0] == 409, r[1]) r = call(client, "DELETE", "/api/v1/bre/plot/delete", json=[plot2[1]["data"]["id"]]) expect("删空 plot 成功", r[0] == 200, r[1]) r = call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteA["id"]]) expect("清空后删 siteA 成功", r[0] == 200, r[1]) print("\n=== 5) 1.3 导入结构化结果 ImportResultSchema ===") from openpyxl import Workbook import io headers5 = ["所属基地", "地块编号/名称", "行向", "行数", "每行株数", "株行距/网格说明", "地块面积(亩)"] wb = Workbook(); ws = wb.active; ws.append(headers5) ws.append([siteB["site_name"], f"IMPT{RUN}1", "东西行", 4, 5, "1.2m", 1.5]) # 合法行 ws.append(["不存在的基地XYZ", f"IMPT{RUN}2", "东西行", 2, 2, "", 0.5]) # 基地不存在 ws.append([siteB["site_name"], f"IMPT{RUN}3", "随便方向", 2, 2, "", 0.5]) # 行向非法 buf = io.BytesIO(); wb.save(buf); buf.seek(0) imp = call(client, "POST", "/api/v1/bre/plot/import", files={ "file": ("import.xlsx", buf.getvalue(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}) expect("import 返回 200", imp[0] == 200, imp[1]) d = imp[1].get("data", {}) expect("valid_count=1", d.get("valid_count") == 1, d) expect("invalid_count=2", d.get("invalid_count") == 2, d) expect("message_list 含坏行原因", len(d.get("message_list", [])) == 2 and all("不存在" in m for m in d["message_list"]) and any("基地名称" in m for m in d["message_list"]) and any("row_orientation" in m for m in d["message_list"]), d) # 重复导入走 update_support 更新(不报"已存在") imp2 = call(client, "POST", "/api/v1/bre/plot/import", files={ "file": ("import.xlsx", buf.getvalue(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}) expect("重复导入 valid_count=1(update_support 更新)", imp2[0] == 200 and imp2[1].get("data", {}).get("valid_count") == 1, imp2[1]) print("\n=== 6) 1.2 导出带筛选参数 ===") exp_r = client.post("/api/v1/bre/plot/export", headers=auth(), params={"site_id": siteB["id"], "plot_code": f"IMPT{RUN}"}) expect("plot export(site_id + LIKE) 200/xlsx", exp_r.status_code == 200 and "spreadsheetml" in exp_r.headers.get("content-type", ""), f"HTTP {exp_r.status_code}") from openpyxl import load_workbook wb6 = load_workbook(io.BytesIO(exp_r.content), read_only=True, data_only=True) ws6 = wb6.active rows6 = [[c.value for c in row] for row in ws6.iter_rows()] wb6.close() hdr6 = rows6[0] if rows6 else [] body6 = [r for r in rows6[1:] if any(v is not None for v in r)] site_col = hdr6.index("所属基地") if "所属基地" in hdr6 else -1 expect("导出列含所属基地", site_col >= 0, hdr6) expect("导出仅含匹配行(1条)", len(body6) == 1 and body6[0][site_col] == siteB["site_name"], body6) exp_all = client.post("/api/v1/bre/plot/export", headers=auth(), params={"site_id": siteB["id"]}) expect("导出全量 siteB 行数=2", exp_all.status_code == 200, str(exp_all.status_code)) wb7 = load_workbook(io.BytesIO(exp_all.content), read_only=True, data_only=True) rows7 = [[c.value for c in row] for row in wb7.active.iter_rows()] wb7.close() body7 = [r for r in rows7[1:] if any(v is not None for v in r)] expect("siteB 全部 2 行导出", len(body7) == 2, body7) print("\n=== 清理 ===") call(client, "DELETE", "/api/v1/bre/plot/delete", json=[plot_id]) for pc in (f"IMPT{RUN}1", f"IMPT{RUN}2", f"IMPT{RUN}3"): rl = call(client, "GET", "/api/v1/bre/plot/list", params={"page": 1, "page_size": 50, "plot_code": pc}) ids = [x["id"] for x in rl[1]["data"]["items"]] if rl[0] == 200 else [] if ids: call(client, "DELETE", "/api/v1/bre/plot/delete", json=ids) call(client, "DELETE", "/api/v1/bre/site/delete", json=[siteB["id"]]) call(client, "DELETE", "/api/v1/bre/site/delete", json=[st[1]["data"]["id"]]) call(client, "DELETE", "/api/v1/bre/site/delete", json=[st2[1]["data"]["id"]]) call(client, "DELETE", "/api/v1/bre/personnel/delete", json=[per[1]["data"]["id"]]) print(f"\nALL {passed} CHECKS PASS \u2705") if __name__ == "__main__": main_()