Files
tps-dashboard/backend/scripts/import_smoke.py
T
2026-05-19 20:05:15 +08:00

255 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Task 2 批量导入冒烟脚本 — httpx(幂等版,可重复跑)
确保后端已启动: py -3.12 -c "import sys; sys.path.insert(0, r'E:\tps-dashboard\backend'); import uvicorn; uvicorn.run('app.main:app', host='127.0.0.1', port=8000, reload=False)"
Phase 2 Task 2 实测清单覆盖:
Step 1: GET /api/imports/template → 模板下载
Step 2: POST /api/imports/episodes → 合法行单行导入
Step 3: POST /api/imports/episodes → 部分失败行(成功+失败混合格式)
Step 4: GET /api/imports/errors/{id} → 失败行 Excel 下载
Step 5: POST /api/imports/episodes → 2024年份缺失拦截 → 400
Step 6: POST /api/imports/episodes → 重复期次拦截 → 409
Step 7: POST /api/imports/episodes → 未知编导软落地 → 200 + editor_id=null
"""
import io
import httpx
import random
from openpyxl import Workbook
BASE = "http://localhost:8000"
def login(username: str = "simonkoson", password: str = "liutong65") -> str:
r = httpx.post(f"{BASE}/api/auth/login", json={"username": username, "password": password})
r.raise_for_status()
return r.cookies.get("milsci_session", "")
def step(name: str, expected_status: int, method: str, url: str, **kwargs):
cookies = kwargs.pop("cookies", {})
if method == "GET":
r = httpx.get(url, cookies=cookies, timeout=30.0, **kwargs)
elif method == "POST":
r = httpx.post(url, cookies=cookies, timeout=30.0, **kwargs)
elif method == "DELETE":
r = httpx.delete(url, cookies=cookies, timeout=30.0, **kwargs)
else:
raise ValueError(method)
status_ok = r.status_code == expected_status
mark = "[OK]" if status_ok else f"[FAIL expected {expected_status} got {r.status_code}]"
print(f" {mark} [{method}] {name}")
if not status_ok:
print(f" Response: {r.text[:300]}")
return r, status_ok
def make_excel(rows: list[list]) -> bytes:
"""构建简单 xlsxrows[0] 为表头,后续为数据。"""
wb = Workbook()
ws = wb.active
for row in rows:
ws.append(row)
output = io.BytesIO()
wb.save(output)
output.seek(0)
return output.read()
def cleanup_test_episodes(cookies: dict):
"""清理测试用 episode_number(幂等清理)。"""
r = httpx.get(f"{BASE}/api/episodes?limit=100", cookies=cookies)
if r.status_code == 200:
for ep in r.json():
num = ep.get("episode_number", 0)
# 清理本次测试可能用到的期号段
if num in (99, 100, 101, 102, 200, 301):
httpx.delete(f"{BASE}/api/episodes/{ep['id']}", cookies=cookies)
def main():
print("=== Login ===")
session_cookie = login()
if not session_cookie:
print("WARNING: session cookie empty")
session_cookie = ""
cookies = {"milsci_session": session_cookie}
# 先清理旧测试数据(幂等)
print("\n=== Cleanup old test episodes ===")
cleanup_test_episodes(cookies)
suffix = random.randint(1000, 9999)
# ---- Step 1: 模板下载 ----
print("\n=== Step 1: Template Download ===")
r, ok = step(
"GET /api/imports/template?type=episodes",
200,
"GET",
f"{BASE}/api/imports/template?type=episodes",
cookies=cookies,
)
content_type = r.headers.get("content-type", "")
is_binary = "application/vnd.openxmlformats" in content_type
print(f" {'[OK]' if is_binary else '[FAIL]'} Content-Type: {content_type}")
print(f" [INFO] Template downloaded ({len(r.content)} bytes)")
# ---- Step 2: 合法单行导入 ----
print("\n=== Step 2: Normal import (1 valid row) ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[99, "《军事科技》实测专用", "2026-06-01", "张颖",
0.75, 3.8, "否", "Plan smoke test"],
])
r, ok = step(
"POST /api/imports/episodes (1 valid row) → 200",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_valid.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
print(f" [INFO] total={result['total_rows']} success={result['success_count']} failed={result['failed_count']}")
ok = (result["success_count"] == 1 and result["failed_count"] == 0)
print(f" {'[OK]' if ok else '[FAIL]'} Row count correct")
# ---- Step 3: 部分失败行回显 ----
print("\n=== Step 3: Partial failure (3 rows: 1 ok + 2 bad) ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[100, "《军事科技》成功行", "2026-07-01", "张颖", 0.71, 3.5, "否", ""],
[101, "无效日期格式行", "不是日期", "张颖", 0.72, 3.6, "否", ""],
[102, "非整数期号行", "invalid-ep-num", "张颖", 0.73, 3.7, "否", ""],
])
r, ok = step(
"POST /api/imports/episodes (1ok+2bad) → 200 with errors",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_mixed.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
batch_id = None
if ok:
result = r.json()
print(f" [INFO] total={result['total_rows']} success={result['success_count']} failed={result['failed_count']}")
ok_count = (result["success_count"] == 1 and result["failed_count"] == 2)
print(f" {'[OK]' if ok_count else '[FAIL]'} Failure count correct")
# 检查 row_number 是否对齐 Excel 行号(第2行=Excel第2行,第3行=Excel第3行,第4行=Excel第4行)
if result["errors"]:
row_nums = sorted([e["row_number"] for e in result["errors"]])
print(f" [INFO] error row_numbers: {row_nums}")
ok_rows = (row_nums == [3, 4])
print(f" {'[OK]' if ok_rows else '[FAIL]'} row_number对齐Excel自然行号(表头第1行,数据从第2行起算)")
batch_id = result.get("batch_id")
if batch_id:
print(f" [INFO] batch_id={batch_id}")
# ---- Step 4: 失败行下载 ----
print("\n=== Step 4: Error Excel Download ===")
if batch_id:
r, ok = step(
f"GET /api/imports/errors/{batch_id} → 200 binary",
200,
"GET",
f"{BASE}/api/imports/errors/{batch_id}",
cookies=cookies,
)
ct = r.headers.get("content-type", "")
is_xlsx = "application/vnd.openxmlformats" in ct
print(f" {'[OK]' if is_xlsx else '[FAIL]'} Content-Type: {ct}")
print(f" [INFO] Error Excel downloaded ({len(r.content)} bytes)")
else:
print(" [SKIP] No batch_id from step 3")
# ---- Step 5: 年份目标缺失拦截 ----
print("\n=== Step 5: Missing yearly target → 400 ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[200, "《军事科技》跨年测试", "2024-01-15", "张颖", 0.68, 3.2, "否", ""],
])
r, ok = step(
"POST with 2024 air_date (no 2024 target) → 400",
400,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_2024.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
detail = r.json().get("detail", "")
has_year_target = "年度目标" in detail and "2024" in detail
print(f" {'[OK]' if has_year_target else '[FAIL]'} detail包含'年度目标'+'2024': {detail[:100]}")
# ---- Step 6: 重复期次拦截 ----
print("\n=== Step 6: Duplicate episode_number ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[99, "《军事科技》重复期次", "2026-06-15", "张颖", 0.76, 3.9, "否", ""],
])
r, ok = step(
"POST episode_number=99 (duplicate in same year) → 409 or row-level rejection",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_duplicate.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
# 数据库唯一约束触发:整行被 rejectsuccess=0, failed=1
has_db_error = result["failed_count"] == 1 and result["success_count"] == 0
has_unique_violation = any("UniqueViolation" in e.get("reason", "") or "idx_episodes_year_number" in e.get("reason", "") for e in result["errors"])
print(f" {'[OK]' if has_unique_violation else '[FAIL]'} 数据库唯一约束触发 (failed_count=1, UniqueViolation in reason)")
# ---- Step 7: 未知编导软落地 ----
print("\n=== Step 7: Unknown editor soft-landing ===")
excel_data = make_excel([
["episode_number", "program_name", "air_date", "editor_name",
"audience_share", "audience_rating", "is_rerun", "notes"],
[301, "《军事科技》编导软引用测试", "2026-09-01", "李不存在", 0.69, 3.1, "否", ""],
])
r, ok = step(
"POST with unknown editor_name='李不存在' → 200",
200,
"POST",
f"{BASE}/api/imports/episodes",
files={"file": ("test_unknown_editor.xlsx", excel_data, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
cookies=cookies,
)
if ok:
result = r.json()
ok = (result["success_count"] == 1 and result["failed_count"] == 0)
print(f" {'[OK]' if ok else '[FAIL]'} success_count=1")
# 验证 editor_id=null(通过列表接口找到刚导入的期次)
r_list, ok_list = step(
"GET /api/episodes?limit=100 → 确认 episode_number=301, editor_id=null, editor_name_snapshot='李不存在'",
200,
"GET",
f"{BASE}/api/episodes?limit=100",
cookies=cookies,
)
if ok_list:
eps = r_list.json()
ep_301 = next((e for e in eps if e.get("episode_number") == 301), None)
if ep_301:
editor_id_null = ep_301.get("editor_id") is None
name_match = ep_301.get("editor_name_snapshot") == "李不存在"
print(f" {'[OK]' if editor_id_null else '[FAIL]'} editor_id=null ({ep_301.get('editor_id')})")
print(f" {'[OK]' if name_match else '[FAIL]'} editor_name_snapshot='李不存在' ({ep_301.get('editor_name_snapshot')})")
else:
print(f" [FAIL] episode_number=301 not found in list (import may have failed)")
print("\n=== All steps done ===")
print("NOTE: This script is辅助. 制片人仍需在 Swagger UI (http://localhost:8000/docs) 手动跑完 8 步确认全绿.")
if __name__ == "__main__":
main()