"""/api/v1/intent/recognize 和 /api/v1/price/step 透传端点测试。 mock 掉对 pricebot 的 httpx 调用,验证(两个端点参数化同跑): 1. 无 token 也能用(MVP 不鉴权,同 coupon/step) 2. pricebot 200 → 响应原样透传,请求 body 原样转发到对应上游路径(去掉 /v1) 3. pricebot 5xx → 502 4. pricebot 网络错误 → 502 5. 非 JSON body → 400 """ from __future__ import annotations import json from unittest.mock import MagicMock, patch import httpx import pytest # (客户端端点, 期望转发到的 pricebot 上游路径) ENDPOINTS = [ ("/api/v1/intent/recognize", "/api/intent/recognize"), ("/api/v1/price/step", "/api/price/step"), ] def _stub_screen_state() -> dict: return { "screen": {"width": 1080, "height": 2340, "density": 3.0}, "foreground": {"package": "com.sankuai.meituan", "activity": ""}, "windows": [], } def _stub_body() -> dict: return { "device_id": "test-device", "trace_id": "test-trace-1", "step": 0, "query": "海底捞", "screen_state": _stub_screen_state(), } @pytest.mark.parametrize("path,upstream", ENDPOINTS) def test_passes_body_through(client, path, upstream) -> None: """无 token(软鉴权放行)+ pricebot 200 → 响应透传(+ app-server 注入 trace_id), body 原样转发(自带 trace_id 时不重签),URL 去掉 /v1。""" fake_resp = { "success": True, "action": {"command": "wait", "params": {"duration_ms": 500}}, "continue": True, } captured: dict = {} async def fake_post(self, url, content=None, **kw): # 透传壳用 content=raw(bytes)转发,不是 json=;这里捕获 content 解回来核对 captured["url"] = url captured["content"] = content mock_resp = MagicMock() mock_resp.status_code = 200 mock_resp.json = lambda: dict(fake_resp) # 每次新副本(端点会 setdefault trace_id 进去) return mock_resp with patch.object(httpx.AsyncClient, "post", fake_post): r = client.post(path, json=_stub_body()) assert r.status_code == 200, r.text body = r.json() assert body["success"] is True assert body["action"] == fake_resp["action"] assert body["trace_id"] == "test-trace-1" # 自带 trace_id → 原样回传, 不 mint # body 原样转发(content=raw bytes;自带 trace_id 时不重新序列化) assert json.loads(captured["content"]) == _stub_body() assert captured["url"].endswith(upstream) # /api/v1/xxx → /api/xxx @pytest.mark.parametrize("path,upstream", ENDPOINTS) def test_pricebot_5xx_returns_502(client, path, upstream) -> None: 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(path, json=_stub_body()) assert r.status_code == 502 assert "pricebot upstream returned 503" in r.json()["detail"] @pytest.mark.parametrize("path,upstream", ENDPOINTS) def test_pricebot_unreachable_returns_502(client, path, upstream) -> None: 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(path, json=_stub_body()) assert r.status_code == 502 assert "pricebot upstream unreachable" in r.json()["detail"] @pytest.mark.parametrize("path,upstream", ENDPOINTS) def test_invalid_json_body(client, path, upstream) -> None: r = client.post( path, headers={"Content-Type": "application/json"}, content=b"not-json", ) assert r.status_code == 400 assert "invalid json body" in r.json()["detail"]