Files
rakuten-api/tests/test_gateway_api.py
T
q792602257andClaude Opus 4.6 07107094a7 实现下单任务网关与本地 worker
按 docs/order-gateway.md 落地:第三个部署单元 app.gateway(:31109)承担任务队列
+ 状态镜像;本地 worker 在 app.trading.worker 内,按 RAKUTEN_ORDER_GATEWAY_URL
决定是否启动。规格 §5 最关键约束已守:租约过期绝不自动重投,恢复只能 reclaim,
worker 收到 lease_count>1 时先核对站点订单。

站点交互(加购/下单/付款/订单列表反查)按规格 §10 留接口缝,site_interact.py
全部 NotImplementedError,verify.py 恒返回 unknown——等真实账号实测后再填,
不写猜测的提交逻辑。

310 个测试全绿,覆盖规格 §9 验收清单 12 条;架构测试守住三方互不 import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-27 16:25:20 +08:00

308 lines
9.4 KiB
Python

"""网关 HTTP API 测试
覆盖规格 §8 的错误码与 §9 验收清单里 HTTP 层能验证的条目:鉴权、响应信封、
6xxx 错误映射、(task_id, state) 重复 report 不产生第二条、reclaim 错误条件。
长轮询与并发抢任务的时序在 test_gateway_leasing.py 单独覆盖。
"""
from __future__ import annotations
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from app.shared.config import get_settings
from app.shared.task_state import OrderState, TaskStatus
TOKEN = get_settings().bearer_token
AUTH = {"Authorization": f"Bearer {TOKEN}"}
@pytest.fixture
def gateway_client(tmp_path: Path, monkeypatch):
"""起一个独立 DB 的网关应用
- monkeypatch 设置 RAKUTEN_GATEWAY_DB_PATH
- get_settings.cache_clear() 让新 env 生效
- TestClient 进入时触发 lifespan(建表、起 sweep 后台任务)
"""
db_path = tmp_path / "gw.db"
monkeypatch.setenv("RAKUTEN_GATEWAY_DB_PATH", str(db_path))
get_settings.cache_clear()
try:
from app.gateway.main import create_app
app = create_app()
with TestClient(app) as client:
yield client
finally:
get_settings.cache_clear()
# ---- 鉴权 ----
def test_health_needs_no_token(gateway_client):
response = gateway_client.get("/health")
assert response.status_code == 200
body = response.json()
assert body["success"] is True
assert body["data"]["status"] in ("ok", "degraded")
assert body["data"]["queued_count"] == 0
@pytest.mark.parametrize(
"path,method",
[
("/api/orders", "POST"),
("/api/orders/lease", "GET"),
("/api/orders/t1", "GET"),
],
)
def test_endpoints_reject_missing_token(gateway_client, path, method):
response = gateway_client.request(method, path)
assert response.status_code == 401
assert response.json()["code"] == 1001
def test_endpoints_reject_wrong_token(gateway_client):
response = gateway_client.post(
"/api/orders",
json={"site": "rakuten", "intent": {}},
headers={"Authorization": "Bearer wrong"},
)
assert response.status_code == 401
# ---- 提交与幂等 ----
def test_submit_returns_task_id_and_status(gateway_client):
response = gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {"k": "v"}},
headers=AUTH,
)
assert response.status_code == 200
body = response.json()
assert body["success"] is True
assert body["code"] == 0
assert body["data"]["task_id"] == "t1"
assert body["data"]["status"] == TaskStatus.QUEUED.value
assert body["data"]["created"] is True
def test_submit_with_same_task_id_is_idempotent(gateway_client):
payload = {"task_id": "t1", "site": "rakuten", "intent": {}}
r1 = gateway_client.post("/api/orders", json=payload, headers=AUTH).json()
r2 = gateway_client.post("/api/orders", json=payload, headers=AUTH).json()
assert r1["data"]["created"] is True
assert r2["data"]["created"] is False
assert r1["data"]["task_id"] == r2["data"]["task_id"]
# ---- 错误码 6xxx 映射 ----
def test_get_unknown_task_returns_6001(gateway_client):
response = gateway_client.get("/api/orders/no-such-task", headers=AUTH)
assert response.status_code == 404
body = response.json()
assert body["success"] is False
assert body["code"] == 6001
def test_report_wrong_worker_returns_6002(gateway_client):
# 提交并 lease 给 w1
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
# w2 试图 report
response = gateway_client.post(
"/api/orders/t1/report",
json={"worker_id": "w2", "state": OrderState.IN_CART.value},
headers=AUTH,
)
assert response.status_code == 409
assert response.json()["code"] == 6002
def test_reclaim_non_stale_returns_6003(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
response = gateway_client.post(
"/api/orders/t1/reclaim",
json={"worker_id": "w1"},
headers=AUTH,
)
assert response.status_code == 409
assert response.json()["code"] == 6003
def test_renew_unknown_task_returns_6001(gateway_client):
response = gateway_client.post(
"/api/orders/no-such/renew",
json={"worker_id": "w1"},
headers=AUTH,
)
assert response.status_code == 404
assert response.json()["code"] == 6001
# ---- report 幂等:同一 (task_id, state) 不产生第二条 ----
def test_report_same_state_does_not_duplicate(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
report_payload = {
"worker_id": "w1",
"state": OrderState.IN_CART.value,
"payable_yen": 9800,
"evidence_ref": "t1/01-cart-add",
"detail": "已加购",
}
r1 = gateway_client.post("/api/orders/t1/report", json=report_payload, headers=AUTH).json()
r2 = gateway_client.post("/api/orders/t1/report", json=report_payload, headers=AUTH).json()
assert r1["data"]["recorded"] is True
assert r2["data"]["recorded"] is False
detail = gateway_client.get("/api/orders/t1", headers=AUTH).json()["data"]
assert len(detail["reports"]) == 1
def test_report_terminal_releases_lease(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
response = gateway_client.post(
"/api/orders/t1/report",
json={
"worker_id": "w1",
"state": OrderState.PAID.value,
"payable_yen": 9800,
"site_order_id": "ord-1",
"terminal": True,
"terminal_status": TaskStatus.SUCCEEDED.value,
"detail": "付款完成",
},
headers=AUTH,
)
assert response.status_code == 200
detail = gateway_client.get("/api/orders/t1", headers=AUTH).json()["data"]
assert detail["status"] == TaskStatus.SUCCEEDED.value
assert detail["lease_owner"] is None
# ---- 列表 ----
def test_list_orders_filters_by_status(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.post(
"/api/orders",
json={"task_id": "t2", "site": "rakuten", "intent": {}},
headers=AUTH,
)
response = gateway_client.get("/api/orders?status=queued", headers=AUTH).json()
assert response["data"]["total"] == 2
assert {item["task_id"] for item in response["data"]["items"]} == {"t1", "t2"}
filtered = gateway_client.get(
"/api/orders?status=leased", headers=AUTH
).json()
assert filtered["data"]["total"] == 0
def test_list_orders_filters_by_site(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.post(
"/api/orders",
json={"task_id": "t2", "site": "rakuma", "intent": {}},
headers=AUTH,
)
response = gateway_client.get("/api/orders?site=rakuma", headers=AUTH).json()
assert response["data"]["total"] == 1
assert response["data"]["items"][0]["site"] == "rakuma"
def test_get_task_detail_includes_full_report_history(gateway_client):
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
gateway_client.post(
"/api/orders/t1/report",
json={"worker_id": "w1", "state": OrderState.IN_CART.value, "detail": "step1"},
headers=AUTH,
)
gateway_client.post(
"/api/orders/t1/report",
json={"worker_id": "w1", "state": OrderState.ORDERED.value, "detail": "step2"},
headers=AUTH,
)
detail = gateway_client.get("/api/orders/t1", headers=AUTH).json()["data"]
assert [r["state"] for r in detail["reports"]] == [
OrderState.IN_CART.value,
OrderState.ORDERED.value,
]
assert detail["latest_state"] == OrderState.ORDERED.value
# ---- lease 立即返回空(无任务时)----
def test_lease_returns_null_when_queue_empty(gateway_client):
"""wait=0 + 无任务时立即返回 data: null"""
response = gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
assert response.status_code == 200
body = response.json()
assert body["success"] is True
assert body["data"] is None
def test_lease_returns_null_when_active_task_blocks(gateway_client):
"""有 leased 任务时,lease 立即返回 data: null(并发度 1)"""
gateway_client.post(
"/api/orders",
json={"task_id": "t1", "site": "rakuten", "intent": {}},
headers=AUTH,
)
gateway_client.get("/api/orders/lease?worker_id=w1&wait=0", headers=AUTH)
response = gateway_client.get("/api/orders/lease?worker_id=w2&wait=0", headers=AUTH)
assert response.status_code == 200
assert response.json()["data"] is None