Files

237 lines
8.3 KiB
Python

"""verify.verify_on_site 单元测试
用桩 gateway(只需要 get_task)和桩 site(只需要 list_recent_orders)跑通全部
分支——不涉及 Playwright/httpx,纯逻辑测试。核对链路本身(intent.item_url →
查任务创建时间 → 拉订单列表 → 按商品 URL 比对)2026-08-13 用真实账号验证过
数据结构(见 test_site_interact.py 里 _parse_order_list 那组测试),但当时账号
只有 1 笔订单,这里 NOT_ORDERED / 多笔命中 / 查询失败这几个分支只覆盖逻辑本身,
没有被真实多单数据跑过。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
import pytest
from app.shared.errors import AppError
from app.trading.worker import verify
from app.trading.worker.models import LeaseTask
from app.trading.worker.site_interact import OrderListEntry, OrderListItem, OrderListWindow
def _make_task(*, item_url: str | None = "https://item.rakuten.co.jp/shop/x/") -> LeaseTask:
intent: dict[str, Any] = {}
if item_url is not None:
intent["item_url"] = item_url
return LeaseTask(task_id="t1", site="rakuten", intent=intent, lease_count=2)
@dataclass
class FakeGateway:
created_at: str | None = "2026-08-01T00:00:00Z"
fail_with: Exception | None = None
calls: list[str] = field(default_factory=list)
async def get_task(self, task_id: str) -> dict[str, Any]:
self.calls.append(task_id)
if self.fail_with is not None:
raise self.fail_with
return {"task_id": task_id, "created_at": self.created_at}
@dataclass
class FakeSite:
window: OrderListWindow | None = None
fail_with: Exception | None = None
received_since: datetime | None = None
async def list_recent_orders(self, *, since: datetime) -> OrderListWindow:
self.received_since = since
if self.fail_with is not None:
raise self.fail_with
assert self.window is not None
return self.window
def _entry(order_number: str, item_url: str) -> OrderListEntry:
return OrderListEntry(
order_number=order_number,
order_date="2026-08-10T00:00:00Z",
items=[OrderListItem(item_url=item_url)],
)
# ---- intent 缺 item_url:不查任何东西,直接 unknown ----
async def test_missing_item_url_returns_unknown_without_calling_gateway():
gateway = FakeGateway()
site = FakeSite()
result = await verify.verify_on_site(_make_task(item_url=None), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
assert gateway.calls == []
# ---- 查任务创建时间失败/不可解析 ----
async def test_get_task_failure_returns_unknown():
gateway = FakeGateway(fail_with=AppError(message="网关挂了", code="X", err_code=9999))
site = FakeSite()
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
assert "网关挂了" in result.detail
async def test_unparseable_created_at_returns_unknown():
gateway = FakeGateway(created_at="不是日期")
site = FakeSite()
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
# ---- 站点订单列表查询失败 ----
async def test_site_list_failure_returns_unknown():
gateway = FakeGateway()
site = FakeSite(fail_with=RuntimeError("Playwright 挂了"))
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
assert "Playwright 挂了" in result.detail
# ---- 命中 1 笔:已下单(2026-08-13 真实账号验证过的分支)----
async def test_exact_one_match_returns_already_ordered():
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[_entry("306087-20260813-0863947697", "https://item.rakuten.co.jp/shop/x/")],
window_fully_covered=True,
)
)
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.ALREADY_ORDERED
assert result.site_order_id == "306087-20260813-0863947697"
async def test_match_ignores_query_string_difference():
"""订单列表的 itemUrl 带 ?variantId=...,intent.item_url 通常不带——只比路径"""
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[_entry("o1", "https://item.rakuten.co.jp/shop/x/?variantId=abc-1")],
window_fully_covered=True,
)
)
result = await verify.verify_on_site(
_make_task(item_url="https://item.rakuten.co.jp/shop/x/"), gateway=gateway, site=site
)
assert result.verdict == verify.VerifyVerdict.ALREADY_ORDERED
async def test_multiple_item_intent_requires_all_items_in_same_order():
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[
OrderListEntry(
order_number="o1",
order_date="2026-08-10T00:00:00Z",
items=[
OrderListItem(item_url="https://item.rakuten.co.jp/shop/x/"),
OrderListItem(item_url="https://item.rakuten.co.jp/shop/y/"),
],
)
],
window_fully_covered=True,
)
)
task = _make_task()
task.intent = {
"items": [
{"item_url": "https://item.rakuten.co.jp/shop/x/"},
{"item_url": "https://item.rakuten.co.jp/shop/y/"},
]
}
result = await verify.verify_on_site(task, gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.ALREADY_ORDERED
assert result.site_order_id == "o1"
async def test_multiple_item_intent_partial_order_match_is_not_ordered():
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[_entry("o1", "https://item.rakuten.co.jp/shop/x/")],
window_fully_covered=True,
)
)
task = _make_task()
task.intent = {
"items": [
{"item_url": "https://item.rakuten.co.jp/shop/x/"},
{"item_url": "https://item.rakuten.co.jp/shop/y/"},
]
}
result = await verify.verify_on_site(task, gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.NOT_ORDERED
# ---- 命中 0 笔且窗口确认覆盖完:未下单 ----
async def test_no_match_and_fully_covered_returns_not_ordered():
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[_entry("o1", "https://item.rakuten.co.jp/other-shop/y/")],
window_fully_covered=True,
)
)
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.NOT_ORDERED
# ---- 命中 0 笔但窗口没确认覆盖完:不能猜没下单 ----
async def test_no_match_and_not_fully_covered_returns_unknown():
gateway = FakeGateway()
site = FakeSite(window=OrderListWindow(entries=[], window_fully_covered=False))
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
# ---- 命中多笔:无法唯一确定 ----
async def test_multiple_matches_returns_unknown():
gateway = FakeGateway()
site = FakeSite(
window=OrderListWindow(
entries=[
_entry("o1", "https://item.rakuten.co.jp/shop/x/"),
_entry("o2", "https://item.rakuten.co.jp/shop/x/"),
],
window_fully_covered=True,
)
)
result = await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert result.verdict == verify.VerifyVerdict.UNKNOWN
assert "o1" in result.detail and "o2" in result.detail
# ---- since 正确传递给 site.list_recent_orders(用任务创建时间,不是别的)----
async def test_since_passed_to_site_matches_task_created_at():
gateway = FakeGateway(created_at="2026-08-05T12:00:00Z")
site = FakeSite(window=OrderListWindow(entries=[], window_fully_covered=True))
await verify.verify_on_site(_make_task(), gateway=gateway, site=site)
assert site.received_since == datetime(2026, 8, 5, 12, 0, 0, tzinfo=timezone.utc)