Files
diary-news/scripts/smoke_ingest.py
xiaji f5fcde1153 test(ingest): 端到端 smoke 脚本
scripts/smoke_ingest.py — 验证 /api/v1/ingest 完整链路,6 步:
  1) 新建 → 201 created
  2) external_id 重复 → 200 duplicate reason=external_id_match
  3) 不带 external_id 新建 → 201 created
  4) 内容指纹重复 → 200 duplicate reason=content_hash_match
  5) 错误 token → 401
  6) body 超 5000 字 → 400 / 422

用法:python scripts/smoke_ingest.py --token <RAW_TOKEN>
默认 host=207.57.129.228 port=19717(直连服务器),
生产走 caddy 时:--scheme https --host your-domain --port 443

需要 owner 在 web 端生成 ingest token(API Push 源的 🔑 Token 按钮)。

smoke 数据会留在 Feed 里(source_ref='smoke', tags 含 'smoke'),
如果想清理:DELETE FROM articles WHERE source_ref='smoke';
或用 source_ref 过滤后手动删。
2026-06-14 20:30:35 +08:00

132 lines
4.8 KiB
Python

"""端到端 smoke:用真实 ingest token 推一条短新闻,验证完整链路。
用法:
python scripts/smoke_ingest.py --token <RAW_TOKEN> [--host <host>] [--port <port>]
行为:
1) POST /api/v1/ingest 推第一条 → 期望 201 created
2) 立即再推同样内容 → 期望 200 duplicate reason=external_id_match
3) 不带 external_id 推不同内容 → 期望 201 created
4) 不带 external_id 重复推同样内容 → 期望 200 duplicate reason=content_hash_match
5) 不带 token → 期望 401
6) body 超 5000 字 → 期望 400 / 422
输出 PASS / FAIL,任何一步失败非零退出。
"""
from __future__ import annotations
import argparse
import json
import sys
import time
from urllib.parse import urljoin
import httpx
def _post(client: httpx.Client, url: str, headers: dict, body: dict, expected: tuple[int, ...]):
"""发起 POST 并断言状态码 + body 关键字段。返回响应对象。"""
r = client.post(url, headers=headers, json=body, timeout=15.0)
status = r.status_code
try:
data = r.json()
except Exception:
data = {"raw": r.text[:200]}
assert status in expected, f"期望 {expected} 实际 {status}: {json.dumps(data, ensure_ascii=False)[:300]}"
return status, data
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--token", required=True, help="X-Ingest-Token 的 raw 值")
ap.add_argument("--host", default="207.57.129.228")
ap.add_argument("--port", type=int, default=19717)
ap.add_argument("--scheme", default="http", help="http / https(走 caddy 时用 https + 域名)")
args = ap.parse_args()
base = f"{args.scheme}://{args.host}:{args.port}" if args.port not in (80, 443) else f"{args.scheme}://{args.host}"
url = urljoin(base + "/", "api/v1/ingest")
auth = {"X-Ingest-Token": args.token, "Content-Type": "application/json"}
ext_id = f"smoke-{int(time.time())}"
payload1 = {
"external_id": ext_id,
"title": f"smoke test {ext_id}",
"body": "smoke test body — 端到端链路验证",
"source_ref": "smoke",
"tags": ["测试", "smoke"],
}
fail = 0
with httpx.Client() as client:
# 1) 新建
try:
s, d = _post(client, url, auth, payload1, (201,))
assert d.get("status") == "created", f"期望 status=created, 实际 {d}"
print(f"[1/6] PASS created article_id={d['article_id']} hash={d['content_hash'][:12]}")
except AssertionError as e:
print(f"[1/6] FAIL created {e}")
fail += 1
# 2) external_id 重复
try:
s, d = _post(client, url, auth, payload1, (200,))
assert d.get("status") == "duplicate" and d.get("reason") == "external_id_match", \
f"期望 external_id_match duplicate, 实际 {d}"
print(f"[2/6] PASS external_id_match article_id={d['article_id']} ext={d.get('matched_external_id')}")
except AssertionError as e:
print(f"[2/6] FAIL external_id_match {e}")
fail += 1
# 3) 不带 external_id 新建
payload3 = {
"title": f"smoke no-ext {int(time.time())}",
"body": f"unique body {time.time_ns()}",
"source_ref": "smoke",
}
try:
s, d = _post(client, url, auth, payload3, (201,))
assert d.get("status") == "created"
print(f"[3/6] PASS no-ext created article_id={d['article_id']}")
except AssertionError as e:
print(f"[3/6] FAIL no-ext created {e}")
fail += 1
# 4) 内容重复(同 payload3)
try:
s, d = _post(client, url, auth, payload3, (200,))
assert d.get("status") == "duplicate" and d.get("reason") == "content_hash_match", \
f"期望 content_hash_match duplicate, 实际 {d}"
print(f"[4/6] PASS content_hash_match article_id={d['article_id']}")
except AssertionError as e:
print(f"[4/6] FAIL content_hash_match {e}")
fail += 1
# 5) 错误 token
try:
s, d = _post(client, url, {"X-Ingest-Token": "definitely-wrong", "Content-Type": "application/json"},
payload1, (401,))
print(f"[5/6] PASS bad-token 401")
except AssertionError as e:
print(f"[5/6] FAIL bad-token {e}")
fail += 1
# 6) body 超长
try:
bad = {"title": "x", "body": "x" * 5001}
s, d = _post(client, url, auth, bad, (400, 422))
print(f"[6/6] PASS body-too-long status={s}")
except AssertionError as e:
print(f"[6/6] FAIL body-too-long {e}")
fail += 1
print(f"\n{'-' * 40}")
if fail:
print(f"FAILED ({fail}/6)")
return 1
print("ALL PASS (6/6)")
return 0
if __name__ == "__main__":
sys.exit(main())