"""比价记录测试:上报派生 / 幂等 / 分页 / 详情 / 鉴权 / 失败帧。 用 sms mock 登录拿 token,再跑各接口闭环。 """ from __future__ import annotations def _login(client, phone: str) -> str: client.post("/api/v1/auth/sms/send", json={"phone": phone}) 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 _auth(token: str) -> dict[str, str]: return {"Authorization": f"Bearer {token}"} def _food_payload(trace_id: str) -> dict: """一次淘宝闪购→美团/京东 的成功比价(美团最便宜)。""" return { "trace_id": trace_id, "business_type": "food", "device_id": "dev-abc", "store_name": "海底捞(朝阳店)", "source_platform_id": "taobao_flash", "source_platform_name": "淘宝闪购", "source_package": "com.taobao.taobao", "source_price": 128.50, "items": [ {"name": "麻辣午餐肉", "qty": 2}, {"name": "黑牛肉卷", "qty": 1, "specs": ["大份"]}, ], "comparison_results": [ {"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan", "price": 123.50, "is_source": False, "rank": 1, "coupon_saved": 7.0, "coupon_name": "外卖大额神券"}, {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao", "price": 128.50, "is_source": True, "rank": 2}, {"platform_id": "jd_waimai", "platform_name": "京东外卖", "package": "com.jingdong.app.mall", "price": 130.00, "is_source": False, "rank": 3}, ], "skipped_dish_count": 1, "skipped_dish_names": ["黑牛肉卷"], "total_dish_count": 3, "information": "在美团找到同店,到手价 ¥123.50", "total_ms": 12345, } def test_report_and_derive(client) -> None: """上报成功比价:服务端派生 best/saved/is_source_best/分,详情可读回。""" token = _login(client, "13800002001") r = client.post("/api/v1/compare/record", json=_food_payload("trace-1"), headers=_auth(token)) assert r.status_code == 200, r.text rec_id = r.json()["id"] assert isinstance(rec_id, int) r = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token)) assert r.status_code == 200, r.text d = r.json() # 金额转分 assert d["source_price_cents"] == 12850 assert d["best_price_cents"] == 12350 # 省 = 源价 - 最优价 = 5.00 元 assert d["saved_amount_cents"] == 500 # 最优是美团(rank=1),非源平台 → 没"源平台最便宜" assert d["best_platform_id"] == "meituan" assert d["is_source_best"] is False assert d["status"] == "success" assert d["information"] == "在美团找到同店,到手价 ¥123.50" assert d["store_name"] == "海底捞(朝阳店)" assert d["total_dish_count"] == 3 assert d["total_ms"] == 12345 assert d["raw_payload"]["total_ms"] == 12345 assert d["skipped_dish_count"] == 1 assert d["skipped_dish_names"] == ["黑牛肉卷"] assert len(d["comparison_results"]) == 3 assert len(d["items"]) == 2 # 详情带 raw_payload 全量 assert d["raw_payload"]["trace_id"] == "trace-1" def test_coupon_saved_passthrough(client) -> None: """红包优惠额 coupon_saved + 来源名 coupon_name 上报→落库→读出;仅目标平台带值,源平台不带(null)。""" token = _login(client, "13800002010") rid = client.post( "/api/v1/compare/record", json=_food_payload("trace-coupon"), headers=_auth(token) ).json()["id"] d = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token)).json() by_pid = {x["platform_id"]: x for x in d["comparison_results"]} assert by_pid["meituan"]["coupon_saved"] == 7.0 # 目标平台带值 assert by_pid["meituan"]["coupon_name"] == "外卖大额神券" # 来源名透传(pricebot#38) assert by_pid["taobao_flash"].get("coupon_saved") is None # 源平台不带/为 null assert by_pid["taobao_flash"].get("coupon_name") is None # 源平台无来源名 def test_source_is_cheapest_no_saving(client) -> None: """源平台本来就最便宜:saved<=0,is_source_best=True,仍记一条 success。""" token = _login(client, "13800002002") payload = _food_payload("trace-2") # 把源平台价改到最低 + rank=1 payload["source_price"] = 100.00 payload["comparison_results"] = [ {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao", "price": 100.00, "is_source": True, "rank": 1}, {"platform_id": "meituan", "platform_name": "美团", "package": "com.sankuai.meituan", "price": 123.50, "is_source": False, "rank": 2}, ] r = client.post("/api/v1/compare/record", json=payload, headers=_auth(token)) rec_id = r.json()["id"] d = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token)).json() assert d["best_platform_id"] == "taobao_flash" assert d["is_source_best"] is True assert d["saved_amount_cents"] == 0 # 100 - 100 assert d["status"] == "success" # 有非源有效价(美团) def test_failed_comparison_recorded(client) -> None: """比价失败(只有源、无目标有效价):status=failed 也落一条。""" token = _login(client, "13800002003") payload = _food_payload("trace-3") payload["comparison_results"] = [ {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao", "price": 128.50, "is_source": True, "rank": 1}, ] r = client.post("/api/v1/compare/record", json=payload, headers=_auth(token)) rec_id = r.json()["id"] d = client.get(f"/api/v1/compare/records/{rec_id}", headers=_auth(token)).json() assert d["status"] == "failed" assert d["best_platform_id"] == "taobao_flash" # 唯一一条 # 源价有、最优价=源价 → saved=0(没采到更便宜目标) assert d["saved_amount_cents"] == 0 def test_idempotent_same_trace(client) -> None: """同 trace_id 重复上报:返回同一 id,只一条记录,后报覆盖。""" token = _login(client, "13800002004") p = _food_payload("trace-dup") r1 = client.post("/api/v1/compare/record", json=p, headers=_auth(token)) id1 = r1.json()["id"] # 改个值再报一次(同 trace) p["store_name"] = "改名后的店" r2 = client.post("/api/v1/compare/record", json=p, headers=_auth(token)) id2 = r2.json()["id"] assert id1 == id2 # 同一条 # 列表里只有一条 page = client.get("/api/v1/compare/records", headers=_auth(token)).json() assert len(page["items"]) == 1 # 覆盖生效 assert page["items"][0]["store_name"] == "改名后的店" def test_records_pagination_and_isolation(client) -> None: """分页 id 倒序 + 游标;只看得到自己的记录。""" token_a = _login(client, "13800002005") for i in range(5): client.post("/api/v1/compare/record", json=_food_payload(f"a-{i}"), headers=_auth(token_a)) # 另一个用户的记录不应混入 token_b = _login(client, "13800002006") client.post("/api/v1/compare/record", json=_food_payload("b-0"), headers=_auth(token_b)) r = client.get("/api/v1/compare/records?limit=3", headers=_auth(token_a)) page1 = r.json() assert len(page1["items"]) == 3 assert page1["next_cursor"] is not None ids = [it["id"] for it in page1["items"]] assert ids == sorted(ids, reverse=True) r = client.get( f"/api/v1/compare/records?limit=3&cursor={page1['next_cursor']}", headers=_auth(token_a), ) page2 = r.json() assert len(page2["items"]) == 2 # A 共 5 条 assert max(it["id"] for it in page2["items"]) < min(ids) # B 只看到自己那一条 pb = client.get("/api/v1/compare/records", headers=_auth(token_b)).json() assert len(pb["items"]) == 1 def test_detail_cross_user_404(client) -> None: """读他人记录详情 → 404(限本人)。""" token_a = _login(client, "13800002007") rid = client.post( "/api/v1/compare/record", json=_food_payload("x-0"), headers=_auth(token_a) ).json()["id"] token_b = _login(client, "13800002008") r = client.get(f"/api/v1/compare/records/{rid}", headers=_auth(token_b)) assert r.status_code == 404 def test_stats_compare_count_and_saved(client) -> None: """比价口径战绩:完成比价数 + 累计发现可省(各成功比价 saved 之和)。""" token = _login(client, "13800002011") # 新用户:无记录 → 全 0(配合「我的」页未比价锁定态) s0 = client.get("/api/v1/compare/stats", headers=_auth(token)).json() assert s0["compare_count"] == 0 assert s0["discovered_saved_cents"] == 0 # 报 2 条成功比价(各省 128.50−123.50=5.00 元=500 分) client.post("/api/v1/compare/record", json=_food_payload("stat-1"), headers=_auth(token)) client.post("/api/v1/compare/record", json=_food_payload("stat-2"), headers=_auth(token)) s = client.get("/api/v1/compare/stats", headers=_auth(token)).json() assert s["compare_count"] == 2 assert s["discovered_saved_cents"] == 1000 # 失败帧(只有源平台)不计入成功比价数 fail = _food_payload("stat-fail") fail["comparison_results"] = [ {"platform_id": "taobao_flash", "platform_name": "淘宝闪购", "package": "com.taobao.taobao", "price": 128.50, "is_source": True, "rank": 1}, ] client.post("/api/v1/compare/record", json=fail, headers=_auth(token)) s2 = client.get("/api/v1/compare/stats", headers=_auth(token)).json() assert s2["compare_count"] == 2 # 仍 2(failed 不计) def test_requires_auth(client) -> None: """不带 token 统一 401。""" assert client.post("/api/v1/compare/record", json={"trace_id": "t"}).status_code == 401 assert client.get("/api/v1/compare/records").status_code == 401 assert client.get("/api/v1/compare/records/1").status_code == 401 assert client.get("/api/v1/compare/stats").status_code == 401 def test_trace_id_required(client) -> None: """缺 trace_id → 422(pydantic 校验)。""" token = _login(client, "13800002009") r = client.post("/api/v1/compare/record", json={"business_type": "food"}, headers=_auth(token)) assert r.status_code == 422