Files
shaguabijia-app-server/tests/test_compare_record.py
T
chenshuobo 1548406f29 美团 feed 改用离线库 + 三 tab 降级兜底(替换 main 的实时版 feed) (#88)
Co-authored-by: lowmaster-chen <1119780489@qq.com>
Co-authored-by: chenshuobo <1119780489@qq.com>
Reviewed-on: #88
Co-authored-by: chenshuobo <chenshuobo@wonderable.ai>
Co-committed-by: chenshuobo <chenshuobo@wonderable.ai>
2026-06-28 10:38:31 +08:00

238 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""比价记录测试:上报派生 / 幂等 / 分页 / 详情 / 鉴权 / 失败帧。
用 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.50123.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