立项配置页初步优化
This commit is contained in:
@@ -0,0 +1,481 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
import asyncpg
|
||||
|
||||
|
||||
BASE = os.getenv("BASE_URL", "http://localhost:8000")
|
||||
ADMIN_EMAIL = os.getenv("EMAIL", "admin@example.com")
|
||||
ADMIN_PASSWORD = os.getenv("PASSWORD", "admin123")
|
||||
STUDY_ID = os.getenv("STUDY_ID", "").strip()
|
||||
DATABASE_URL = (os.getenv("DATABASE_URL") or "postgresql://ctms_user:secret_password@db/ctms_db").replace("+asyncpg", "")
|
||||
|
||||
|
||||
def request_json(path: str, *, method: str = "GET", token: str | None = None, payload: dict | None = None) -> tuple[int, dict]:
|
||||
url = f"{BASE}{path}"
|
||||
headers = {}
|
||||
body = None
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
if payload is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(url, method=method, headers=headers, data=body)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as resp:
|
||||
text = resp.read().decode() or "{}"
|
||||
return resp.getcode(), json.loads(text)
|
||||
except urllib.error.HTTPError as exc:
|
||||
text = exc.read().decode() or "{}"
|
||||
try:
|
||||
body_json = json.loads(text)
|
||||
except Exception:
|
||||
body_json = {"raw": text}
|
||||
return exc.code, body_json
|
||||
|
||||
|
||||
def request_bytes(path: str, *, method: str = "GET", token: str | None = None) -> tuple[int, bytes]:
|
||||
url = f"{BASE}{path}"
|
||||
headers = {}
|
||||
if token:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, method=method, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.getcode(), resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, exc.read()
|
||||
|
||||
|
||||
def request_multipart(
|
||||
path: str,
|
||||
*,
|
||||
token: str,
|
||||
file_field: str,
|
||||
file_name: str,
|
||||
file_content: bytes,
|
||||
expected_version: int,
|
||||
) -> tuple[int, dict]:
|
||||
boundary = f"----ctms-boundary-{uuid.uuid4().hex}"
|
||||
chunks: list[bytes] = []
|
||||
chunks.append(
|
||||
(
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="expected_version"\r\n\r\n'
|
||||
f"{expected_version}\r\n"
|
||||
).encode("utf-8")
|
||||
)
|
||||
chunks.append(
|
||||
(
|
||||
f"--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="{file_field}"; filename="{file_name}"\r\n'
|
||||
f"Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\r\n\r\n"
|
||||
).encode("utf-8")
|
||||
)
|
||||
chunks.append(file_content)
|
||||
chunks.append(b"\r\n")
|
||||
chunks.append(f"--{boundary}--\r\n".encode("utf-8"))
|
||||
body = b"".join(chunks)
|
||||
|
||||
url = f"{BASE}{path}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
||||
}
|
||||
req = urllib.request.Request(url, method="POST", headers=headers, data=body)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
text = resp.read().decode() or "{}"
|
||||
return resp.getcode(), json.loads(text)
|
||||
except urllib.error.HTTPError as exc:
|
||||
text = exc.read().decode() or "{}"
|
||||
try:
|
||||
body_json = json.loads(text)
|
||||
except Exception:
|
||||
body_json = {"raw": text}
|
||||
return exc.code, body_json
|
||||
|
||||
|
||||
def assert_or_exit(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
print(f"[FAIL] {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def extract_items(payload: dict) -> list[dict]:
|
||||
if isinstance(payload, list):
|
||||
return payload
|
||||
items = payload.get("items")
|
||||
if isinstance(items, list):
|
||||
return items
|
||||
return []
|
||||
|
||||
|
||||
async def _db_fetch(sql: str, *args):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
rows = await conn.fetch(sql, *args)
|
||||
return [dict(row) for row in rows]
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
def db_fetch(sql: str, *args) -> list[dict]:
|
||||
return asyncio.run(_db_fetch(sql, *args))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(f"[config] BASE={BASE} EMAIL={ADMIN_EMAIL}")
|
||||
status, login = request_json(
|
||||
"/api/v1/auth/login",
|
||||
method="POST",
|
||||
payload={"email": ADMIN_EMAIL, "password": ADMIN_PASSWORD},
|
||||
)
|
||||
assert_or_exit(status == 200, f"登录失败 status={status} body={login}")
|
||||
token = login["access_token"]
|
||||
|
||||
if STUDY_ID:
|
||||
study_id = STUDY_ID
|
||||
else:
|
||||
status, studies = request_json("/api/v1/studies/?skip=0&limit=1", token=token)
|
||||
assert_or_exit(status == 200, f"获取项目列表失败 status={status} body={studies}")
|
||||
items = studies.get("items") or []
|
||||
assert_or_exit(bool(items), "未找到可用项目,请设置环境变量 STUDY_ID")
|
||||
study_id = items[0]["id"]
|
||||
print(f"[1/12] study_id={study_id}")
|
||||
|
||||
status, study_before = request_json(f"/api/v1/studies/{study_id}", token=token)
|
||||
assert_or_exit(status == 200, f"获取项目详情失败 status={status} body={study_before}")
|
||||
|
||||
status, sites_resp = request_json(f"/api/v1/studies/{study_id}/sites/?include_inactive=true&skip=0&limit=200", token=token)
|
||||
assert_or_exit(status == 200, f"获取中心列表失败 status={status} body={sites_resp}")
|
||||
sites = extract_items(sites_resp if isinstance(sites_resp, dict) else {"items": sites_resp})
|
||||
active_sites = [item for item in sites if item.get("is_active") is True]
|
||||
assert_or_exit(len(active_sites) >= 1, "至少需要1个活跃中心以验证中心目标联动")
|
||||
|
||||
status, cfg = request_json(f"/api/v1/studies/{study_id}/setup-config", token=token)
|
||||
assert_or_exit(status == 200, f"获取配置失败 status={status} body={cfg}")
|
||||
version = cfg["version"]
|
||||
print(f"[2/12] get_ok version={version}")
|
||||
|
||||
payload = {"expected_version": version, "data": cfg["data"]}
|
||||
payload["data"]["projectMilestones"] = [
|
||||
{
|
||||
"id": "smoke-project-ms-1",
|
||||
"name": "立项启动",
|
||||
"planDate": "2026-02-20",
|
||||
"owner": ADMIN_EMAIL,
|
||||
"remark": "project-ms-remark",
|
||||
"status": "进行中",
|
||||
}
|
||||
]
|
||||
payload["data"]["enrollmentPlan"]["monthlyGoalNote"] = f"smoke-test-note-v{version}"
|
||||
payload["data"]["enrollmentPlan"]["stageBreakdown"] = f"smoke-stage-v{version}"
|
||||
payload["data"]["enrollmentPlan"]["totalTarget"] = max(1, int(study_before.get("planned_enrollment_count") or 0) + 7)
|
||||
payload["data"]["enrollmentPlan"]["startDate"] = "2026-03-01"
|
||||
payload["data"]["enrollmentPlan"]["endDate"] = "2026-09-30"
|
||||
payload["data"]["siteMilestones"] = [
|
||||
{
|
||||
"id": "smoke-site-ms-1",
|
||||
"siteId": active_sites[0]["id"],
|
||||
"siteName": active_sites[0].get("name") or "",
|
||||
"milestone": "中心启动",
|
||||
"planDate": "2026-03-05",
|
||||
"owner": ADMIN_EMAIL,
|
||||
"remark": "site-ms-remark",
|
||||
}
|
||||
]
|
||||
payload["data"]["siteEnrollmentPlans"] = [
|
||||
{
|
||||
"id": f"smoke-plan-{idx+1}",
|
||||
"siteId": site["id"],
|
||||
"siteName": site.get("name") or "",
|
||||
"target": 10 if idx == 0 else 8,
|
||||
"startDate": "2026-03-01",
|
||||
"endDate": "2026-09-30",
|
||||
"note": "smoke-site-plan",
|
||||
}
|
||||
for idx, site in enumerate(active_sites[:2])
|
||||
]
|
||||
payload["data"]["monitoringStrategies"] = [
|
||||
{
|
||||
"id": "smoke-monitoring-1",
|
||||
"strategyType": "风险监查",
|
||||
"detail": "基于关键风险触发",
|
||||
"frequency": "按触发",
|
||||
"updatedAt": "",
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
payload["data"]["centerConfirm"] = [
|
||||
{
|
||||
"id": "smoke-center-confirm-1",
|
||||
"siteId": active_sites[0]["id"],
|
||||
"siteName": active_sites[0].get("name") or "",
|
||||
"confirmer": "System Admin",
|
||||
"confirmStatus": "已确认",
|
||||
"confirmDate": "2026-03-15",
|
||||
"note": "smoke-center-confirm",
|
||||
}
|
||||
]
|
||||
|
||||
targeted_site_ids = {item["siteId"] for item in payload["data"]["siteEnrollmentPlans"]}
|
||||
before_site_map = {str(item.get("id")): item for item in sites if str(item.get("id")) in targeted_site_ids}
|
||||
|
||||
status, updated = request_json(f"/api/v1/studies/{study_id}/setup-config", token=token, method="PUT", payload=payload)
|
||||
assert_or_exit(status == 200, f"保存草稿失败 status={status} body={updated}")
|
||||
latest_version = updated["version"]
|
||||
print(f"[3/12] put_ok version={latest_version}")
|
||||
|
||||
status, study_after_save = request_json(f"/api/v1/studies/{study_id}", token=token)
|
||||
assert_or_exit(status == 200, f"保存后获取项目详情失败 status={status} body={study_after_save}")
|
||||
assert_or_exit(
|
||||
study_after_save.get("planned_enrollment_count") == study_before.get("planned_enrollment_count")
|
||||
and study_after_save.get("plan_start_date") == study_before.get("plan_start_date")
|
||||
and study_after_save.get("plan_end_date") == study_before.get("plan_end_date")
|
||||
and study_after_save.get("enrollment_monthly_goal_note") == study_before.get("enrollment_monthly_goal_note")
|
||||
and study_after_save.get("enrollment_stage_breakdown") == study_before.get("enrollment_stage_breakdown"),
|
||||
"保存草稿不应写入项目业务表字段",
|
||||
)
|
||||
|
||||
status, sites_after_save_resp = request_json(
|
||||
f"/api/v1/studies/{study_id}/sites/?include_inactive=true&skip=0&limit=200", token=token
|
||||
)
|
||||
assert_or_exit(status == 200, f"保存后获取中心失败 status={status} body={sites_after_save_resp}")
|
||||
sites_after_save = extract_items(sites_after_save_resp if isinstance(sites_after_save_resp, dict) else {"items": sites_after_save_resp})
|
||||
after_save_site_map = {str(item.get("id")): item for item in sites_after_save if str(item.get("id")) in targeted_site_ids}
|
||||
for site_id in targeted_site_ids:
|
||||
before_item = before_site_map.get(site_id)
|
||||
after_item = after_save_site_map.get(site_id)
|
||||
if not before_item or not after_item:
|
||||
continue
|
||||
assert_or_exit(
|
||||
after_item.get("enrollment_target") == before_item.get("enrollment_target")
|
||||
and after_item.get("enrollment_plan_start_date") == before_item.get("enrollment_plan_start_date")
|
||||
and after_item.get("enrollment_plan_end_date") == before_item.get("enrollment_plan_end_date")
|
||||
and after_item.get("enrollment_plan_note") == before_item.get("enrollment_plan_note"),
|
||||
f"保存草稿不应写入中心业务表字段 site={site_id}",
|
||||
)
|
||||
print("[4/12] save_no_projection_ok")
|
||||
|
||||
status, stale_result = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config",
|
||||
token=token,
|
||||
method="PUT",
|
||||
payload={"expected_version": version, "data": updated["data"]},
|
||||
)
|
||||
stale_code = (stale_result.get("detail") or {}).get("code") if isinstance(stale_result.get("detail"), dict) else ""
|
||||
assert_or_exit(status == 409 and stale_code == "SETUP_CONFIG_VERSION_CONFLICT", f"冲突校验失败 status={status} body={stale_result}")
|
||||
print("[5/12] conflict_409_ok")
|
||||
|
||||
invalid_payload = json.loads(json.dumps(updated))
|
||||
invalid_payload["expected_version"] = latest_version
|
||||
invalid_payload["data"]["enrollmentPlan"]["startDate"] = "2026-12-31"
|
||||
invalid_payload["data"]["enrollmentPlan"]["endDate"] = "2026-01-01"
|
||||
status, invalid_result = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config",
|
||||
token=token,
|
||||
method="PUT",
|
||||
payload={"expected_version": invalid_payload["expected_version"], "data": invalid_payload["data"]},
|
||||
)
|
||||
detail = invalid_result.get("detail") if isinstance(invalid_result, dict) else None
|
||||
err_code = detail.get("code") if isinstance(detail, dict) else ""
|
||||
err_list = detail.get("errors") if isinstance(detail, dict) else []
|
||||
has_enrollment_date_error = any((item.get("field") == "enrollmentPlan.endDate") for item in (err_list or []))
|
||||
assert_or_exit(status == 422 and err_code == "VALIDATION_ERROR" and has_enrollment_date_error, f"422校验失败 status={status} body={invalid_result}")
|
||||
print("[6/12] validation_422_ok")
|
||||
|
||||
status, published = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config/publish",
|
||||
token=token,
|
||||
method="POST",
|
||||
payload={"expected_version": latest_version},
|
||||
)
|
||||
assert_or_exit(status == 200 and published.get("publish_status") == "PUBLISHED", f"发布失败 status={status} body={published}")
|
||||
projection_status = str(published.get("projection_status") or "").lower()
|
||||
assert_or_exit(
|
||||
projection_status in {"success", "partial_success"},
|
||||
f"发布联动状态异常 projection_status={published.get('projection_status')} body={published}",
|
||||
)
|
||||
first_published_version = published["version"]
|
||||
print(f"[7/12] first_publish_ok version={first_published_version}")
|
||||
|
||||
status, study_detail = request_json(f"/api/v1/studies/{study_id}", token=token)
|
||||
assert_or_exit(status == 200, f"获取项目详情失败 status={status} body={study_detail}")
|
||||
assert_or_exit(
|
||||
study_detail.get("planned_enrollment_count") == payload["data"]["enrollmentPlan"]["totalTarget"],
|
||||
f"项目计划入组联动失败 expected={payload['data']['enrollmentPlan']['totalTarget']} actual={study_detail.get('planned_enrollment_count')}",
|
||||
)
|
||||
assert_or_exit(
|
||||
study_detail.get("plan_start_date") == payload["data"]["enrollmentPlan"]["startDate"]
|
||||
and study_detail.get("plan_end_date") == payload["data"]["enrollmentPlan"]["endDate"],
|
||||
f"项目计划日期联动失败 expected=({payload['data']['enrollmentPlan']['startDate']},{payload['data']['enrollmentPlan']['endDate']}) actual=({study_detail.get('plan_start_date')},{study_detail.get('plan_end_date')})",
|
||||
)
|
||||
assert_or_exit(
|
||||
study_detail.get("enrollment_monthly_goal_note") == payload["data"]["enrollmentPlan"]["monthlyGoalNote"]
|
||||
and study_detail.get("enrollment_stage_breakdown") == payload["data"]["enrollmentPlan"]["stageBreakdown"],
|
||||
"项目入组补充说明联动失败",
|
||||
)
|
||||
|
||||
status, sites_after_resp = request_json(f"/api/v1/studies/{study_id}/sites/?include_inactive=true&skip=0&limit=200", token=token)
|
||||
assert_or_exit(status == 200, f"发布后获取中心失败 status={status} body={sites_after_resp}")
|
||||
sites_after = extract_items(sites_after_resp if isinstance(sites_after_resp, dict) else {"items": sites_after_resp})
|
||||
site_target_map = {str(item.get("id")): item.get("enrollment_target") for item in sites_after}
|
||||
site_plan_start_map = {str(item.get("id")): item.get("enrollment_plan_start_date") for item in sites_after}
|
||||
site_plan_end_map = {str(item.get("id")): item.get("enrollment_plan_end_date") for item in sites_after}
|
||||
site_plan_note_map = {str(item.get("id")): item.get("enrollment_plan_note") for item in sites_after}
|
||||
for plan in payload["data"]["siteEnrollmentPlans"]:
|
||||
actual_target = site_target_map.get(str(plan["siteId"]))
|
||||
assert_or_exit(
|
||||
actual_target == plan["target"],
|
||||
f"中心目标联动失败 site={plan['siteId']} expected={plan['target']} actual={actual_target}",
|
||||
)
|
||||
assert_or_exit(
|
||||
site_plan_start_map.get(str(plan["siteId"])) == plan["startDate"]
|
||||
and site_plan_end_map.get(str(plan["siteId"])) == plan["endDate"]
|
||||
and site_plan_note_map.get(str(plan["siteId"])) == plan["note"],
|
||||
f"中心入组计划附加字段联动失败 site={plan['siteId']}",
|
||||
)
|
||||
|
||||
monitoring_rows = db_fetch(
|
||||
"""
|
||||
SELECT strategy_type, detail, frequency, enabled
|
||||
FROM study_monitoring_strategies
|
||||
WHERE study_id = $1::uuid
|
||||
""",
|
||||
study_id,
|
||||
)
|
||||
assert_or_exit(len(monitoring_rows) == 1, f"监查策略联动失败 rows={monitoring_rows}")
|
||||
assert_or_exit(
|
||||
monitoring_rows[0]["strategy_type"] == "风险监查"
|
||||
and monitoring_rows[0]["detail"] == "基于关键风险触发"
|
||||
and monitoring_rows[0]["frequency"] == "按触发"
|
||||
and monitoring_rows[0]["enabled"] is True,
|
||||
f"监查策略字段联动失败 rows={monitoring_rows}",
|
||||
)
|
||||
|
||||
center_confirm_rows = db_fetch(
|
||||
"""
|
||||
SELECT site_id::text AS site_id, confirmer, confirm_status, confirm_date::text AS confirm_date, note
|
||||
FROM study_center_confirms
|
||||
WHERE study_id = $1::uuid
|
||||
""",
|
||||
study_id,
|
||||
)
|
||||
assert_or_exit(len(center_confirm_rows) == 1, f"中心确认联动失败 rows={center_confirm_rows}")
|
||||
assert_or_exit(
|
||||
center_confirm_rows[0]["site_id"] == active_sites[0]["id"]
|
||||
and center_confirm_rows[0]["confirm_status"] == "已确认"
|
||||
and center_confirm_rows[0]["confirm_date"] == "2026-03-15"
|
||||
and center_confirm_rows[0]["note"] == "smoke-center-confirm",
|
||||
f"中心确认字段联动失败 rows={center_confirm_rows}",
|
||||
)
|
||||
|
||||
milestone_rows = db_fetch(
|
||||
"""
|
||||
SELECT type, name, owner_id::text AS owner_id, owner_name, notes
|
||||
FROM milestones
|
||||
WHERE study_id = $1::uuid
|
||||
AND type IN ('SETUP_PROJECT_MILESTONE', 'SETUP_SITE_MILESTONE')
|
||||
ORDER BY type, name
|
||||
""",
|
||||
study_id,
|
||||
)
|
||||
milestone_map = {(row["type"], row["name"]): row for row in milestone_rows}
|
||||
project_row = milestone_map.get(("SETUP_PROJECT_MILESTONE", "立项启动"))
|
||||
site_row = milestone_map.get(("SETUP_SITE_MILESTONE", "中心启动"))
|
||||
assert_or_exit(project_row is not None and site_row is not None, f"里程碑联动失败 rows={milestone_rows}")
|
||||
assert_or_exit(
|
||||
project_row.get("owner_name") == ADMIN_EMAIL
|
||||
and bool(project_row.get("owner_id"))
|
||||
and project_row.get("notes") == "project-ms-remark",
|
||||
f"项目里程碑owner结构化失败 row={project_row}",
|
||||
)
|
||||
assert_or_exit(
|
||||
site_row.get("owner_name") == ADMIN_EMAIL
|
||||
and bool(site_row.get("owner_id"))
|
||||
and site_row.get("notes") == "site-ms-remark",
|
||||
f"中心里程碑owner结构化失败 row={site_row}",
|
||||
)
|
||||
print("[8/12] publish_projection_db_assert_ok")
|
||||
|
||||
payload_v2 = {"expected_version": published["version"], "data": published["data"]}
|
||||
payload_v2["data"]["enrollmentPlan"]["totalTarget"] = payload["data"]["enrollmentPlan"]["totalTarget"] + 3
|
||||
payload_v2["data"]["enrollmentPlan"]["monthlyGoalNote"] = payload["data"]["enrollmentPlan"]["monthlyGoalNote"] + "-v2"
|
||||
if payload_v2["data"]["siteEnrollmentPlans"]:
|
||||
payload_v2["data"]["siteEnrollmentPlans"][0]["target"] = payload_v2["data"]["siteEnrollmentPlans"][0]["target"] + 2
|
||||
|
||||
status, updated_v2 = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config",
|
||||
token=token,
|
||||
method="PUT",
|
||||
payload=payload_v2,
|
||||
)
|
||||
assert_or_exit(status == 200, f"二次保存草稿失败 status={status} body={updated_v2}")
|
||||
status, published_v2 = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config/publish",
|
||||
token=token,
|
||||
method="POST",
|
||||
payload={"expected_version": updated_v2["version"]},
|
||||
)
|
||||
assert_or_exit(status == 200 and published_v2.get("publish_status") == "PUBLISHED", f"二次发布失败 status={status} body={published_v2}")
|
||||
|
||||
status, rolled = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config/rollback",
|
||||
token=token,
|
||||
method="POST",
|
||||
payload={"expected_version": published_v2["version"], "target_version": first_published_version},
|
||||
)
|
||||
assert_or_exit(status == 200, f"回滚失败 status={status} body={rolled}")
|
||||
assert_or_exit(
|
||||
rolled.get("publish_status") == "DRAFT"
|
||||
and (rolled.get("data") or {}).get("enrollmentPlan", {}).get("totalTarget") == payload["data"]["enrollmentPlan"]["totalTarget"],
|
||||
f"回滚结果异常 body={rolled}",
|
||||
)
|
||||
print("[9/12] rollback_ok")
|
||||
|
||||
status, exported_bytes = request_bytes(f"/api/v1/studies/{study_id}/setup-config/export-excel", token=token)
|
||||
assert_or_exit(status == 200 and len(exported_bytes) > 0, f"导出Excel失败 status={status} bytes={len(exported_bytes)}")
|
||||
with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp:
|
||||
tmp.write(exported_bytes)
|
||||
tmp_path = tmp.name
|
||||
print(f"[10/12] export_excel_ok file={tmp_path}")
|
||||
|
||||
status, imported = request_multipart(
|
||||
f"/api/v1/studies/{study_id}/setup-config/import-excel",
|
||||
token=token,
|
||||
file_field="file",
|
||||
file_name="setup-config-smoke.xlsx",
|
||||
file_content=exported_bytes,
|
||||
expected_version=rolled["version"],
|
||||
)
|
||||
assert_or_exit(status == 200 and isinstance(imported.get("version"), int), f"导入失败 status={status} body={imported}")
|
||||
print(f"[11/12] import_excel_ok version={imported['version']}")
|
||||
|
||||
status, _ = request_json(f"/api/v1/studies/{study_id}/lock", token=token, method="PATCH", payload={})
|
||||
assert_or_exit(status == 200, f"锁定失败 status={status}")
|
||||
try:
|
||||
status, locked_result = request_json(
|
||||
f"/api/v1/studies/{study_id}/setup-config",
|
||||
token=token,
|
||||
method="PUT",
|
||||
payload={"expected_version": imported["version"], "data": imported["data"]},
|
||||
)
|
||||
assert_or_exit(status == 403, f"锁定后写入应403,实际 status={status} body={locked_result}")
|
||||
print("[12/12] lock_403_check_ok")
|
||||
finally:
|
||||
unlock_status, unlock_body = request_json(f"/api/v1/studies/{study_id}/unlock", token=token, method="PATCH", payload={})
|
||||
assert_or_exit(unlock_status == 200, f"解锁失败 status={unlock_status} body={unlock_body}")
|
||||
|
||||
print("setup-config smoke test passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user