190 lines
11 KiB
Python
190 lines
11 KiB
Python
"""波次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_()
|