93bf991fe1
对齐 compare 链路(_forward 的 setdefault 注入,每步响应都带 trace_id):coupon_step 透传响应在 resp.json() 后 setdefault 注入本次任务 trace_id,客户端任一帧都能从响应 拿到全链 id。**只回显请求里带的、不 mint**——step 是循环接口,每帧签新 id 会把一次 任务打散;领券 trace_id 的唯一签发点保持在 /coupon/session (status=started)。带 isinstance dict + 非空 trace_id 双防御(pricebot 响应顶层本无 trace_id 字段,setdefault 不会覆盖任何上游值)。 顺手修复 test_coupon_proxy.py 一个 pre-existing 红测试:coupon_step 转发早已改为 content=raw 原始字节透传,但 fake_post 仍只捕获 json= 参数 → captured["json"] 恒 None、body 转发断言一直失败(git stash 验证不带本轮改动同样红)。fake 改捕 content、 断言按字节 json.loads 后对比;transparently-passes-through 断言更新为"透传 + 顶层 回显 trace_id"新契约。 领券相关测试 22 passed。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
"""/api/v1/coupon/step 透传端点测试。
|
|
|
|
mock 掉对 pricebot 的 httpx 调用,验证:
|
|
1. 无 token → 401
|
|
2. 带 token + pricebot 200 → 响应原样透传,请求 body 原样转发
|
|
3. pricebot 5xx → 502
|
|
4. pricebot 网络错误 → 502
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
|
|
def _stub_screen_state() -> dict:
|
|
"""最简的 screen_state(领券业务用,跟外卖/电商同结构)。"""
|
|
return {
|
|
"screen": {"width": 1080, "height": 2340, "density": 3.0},
|
|
"foreground": {"package": "x", "activity": ""},
|
|
"windows": [],
|
|
}
|
|
|
|
|
|
def _stub_request_body() -> dict:
|
|
return {
|
|
"device_id": "test-device",
|
|
"trace_id": "test-trace-1",
|
|
"step": 0,
|
|
"screen_state": _stub_screen_state(),
|
|
}
|
|
|
|
|
|
@pytest.fixture()
|
|
def access_token(client) -> str:
|
|
"""sms 登录拿一个 access_token。每次新 phone 避免冷却 429。"""
|
|
phone = f"139{int(time.time() * 1000) % 100000000:08d}"
|
|
r = client.post("/api/v1/auth/sms/send", json={"phone": phone})
|
|
assert r.status_code == 200, r.text
|
|
r = client.post("/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"})
|
|
assert r.status_code == 200, r.text
|
|
return r.json()["access_token"]
|
|
|
|
|
|
def test_coupon_step_no_auth_required(client) -> None:
|
|
"""MVP 不鉴权:不带 token 也能转发(已去掉 CurrentUser,device_id 透传)。
|
|
|
|
历史:本测试原断言"无 token → 401",但 coupon/step 已去鉴权(见 docs/待办与
|
|
技术债.md「已解决」),401 断言已过时,改为验证不带 token 也能正常透传。
|
|
"""
|
|
async def fake_post(self, url, json=None, **kw):
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json = lambda: {"success": True}
|
|
return mock_resp
|
|
|
|
with patch.object(httpx.AsyncClient, "post", fake_post):
|
|
r = client.post("/api/v1/coupon/step", json=_stub_request_body())
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
def test_coupon_step_passes_body_through(client, access_token) -> None:
|
|
"""带 token + pricebot 200 → 请求 body 原样转发到 /api/coupon/step;
|
|
响应在透传基础上顶层回显本次任务 trace_id(setdefault 注入,其余字段原样)。"""
|
|
fake_pricebot_resp = {
|
|
"success": True,
|
|
"action": {
|
|
"command": "launch",
|
|
"params": {"app": "美团", "deeplink": "imeituan://test"},
|
|
},
|
|
"continue": True,
|
|
"status": {
|
|
"platform": "coupon",
|
|
"progress": {
|
|
"current": 1,
|
|
"total": 2,
|
|
"current_coupon_id": "mt_banjia",
|
|
"current_coupon_name": "美团半价周末",
|
|
},
|
|
},
|
|
}
|
|
|
|
captured: dict = {}
|
|
|
|
# ⚠️ coupon_step 转发用 content=raw(原始字节透传,不重新 dumps),不是 json= ——
|
|
# fake 必须捕 content。旧 fake 只捕 json= 导致 captured["json"] 恒 None,本测试
|
|
# 自 content=raw 优化后一直红着(pre-existing),本次顺手修正。
|
|
async def fake_post(self, url, content=None, **kw):
|
|
captured["url"] = url
|
|
captured["content"] = content
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 200
|
|
mock_resp.json = lambda: fake_pricebot_resp
|
|
return mock_resp
|
|
|
|
with patch.object(httpx.AsyncClient, "post", fake_post):
|
|
r = client.post(
|
|
"/api/v1/coupon/step",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
json=_stub_request_body(),
|
|
)
|
|
|
|
assert r.status_code == 200, r.text
|
|
# 顶层多出 trace_id 回显(值=请求带的;不 mint,签发点唯一在 /coupon/session started)
|
|
assert r.json() == {**fake_pricebot_resp, "trace_id": "test-trace-1"}
|
|
# 验证请求被原样转发(body 字节不动 + URL 指向 pricebot)
|
|
assert json.loads(captured["content"]) == _stub_request_body()
|
|
assert captured["url"].endswith("/api/coupon/step")
|
|
|
|
|
|
def test_coupon_step_pricebot_5xx_returns_502(client, access_token) -> None:
|
|
"""pricebot 返 503 → 我们返 502。"""
|
|
async def fake_post(self, url, json=None, **kw):
|
|
mock_resp = MagicMock()
|
|
mock_resp.status_code = 503
|
|
mock_resp.text = "service unavailable"
|
|
return mock_resp
|
|
|
|
with patch.object(httpx.AsyncClient, "post", fake_post):
|
|
r = client.post(
|
|
"/api/v1/coupon/step",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
json=_stub_request_body(),
|
|
)
|
|
|
|
assert r.status_code == 502
|
|
assert "pricebot upstream returned 503" in r.json()["detail"]
|
|
|
|
|
|
def test_coupon_step_pricebot_unreachable_returns_502(client, access_token) -> None:
|
|
"""pricebot 网络不可达 → 502。"""
|
|
async def fake_post(self, url, json=None, **kw):
|
|
raise httpx.ConnectError("connection refused")
|
|
|
|
with patch.object(httpx.AsyncClient, "post", fake_post):
|
|
r = client.post(
|
|
"/api/v1/coupon/step",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
json=_stub_request_body(),
|
|
)
|
|
|
|
assert r.status_code == 502
|
|
assert "pricebot upstream unreachable" in r.json()["detail"]
|
|
|
|
|
|
def test_coupon_step_invalid_json_body(client, access_token) -> None:
|
|
"""非 JSON body → 400(在转发前就拒绝)。"""
|
|
r = client.post(
|
|
"/api/v1/coupon/step",
|
|
headers={
|
|
"Authorization": f"Bearer {access_token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
content=b"not-json",
|
|
)
|
|
assert r.status_code == 400
|
|
assert "invalid json body" in r.json()["detail"]
|