结算流程用真实下单证据修正:新卡代填 iframe vault、订单号正则、登录态判据
- 新卡代填改用真实 DOM 结构:卡号/有效期分别托管在 Rakuten PCI 代付 vault 的 跨域 iframe 里,此前按 autocomplete/name 猜的 selector 在主文档里根本找不到 元素;持卡人姓名字段改用「名義人」标签相对定位,不用 placeholder 示例文案 (TARO RAKUTEN 只是示例用户名,不是稳定标识) - 订单号正则修正 实体导致的分隔符匹配失败(2026-08-13 真实下单验证) - login_runner._is_logged_in 改用与 AuthSession 一致的 __INITIAL_STATE__ 判据, 修掉此前误判「已登录」导致保存无效 storage_state 的问题 - 新增 CheckoutBlockedError:站点风控拦截(session upgrade/3DS)转 needs_human, 不当普通失败重试 - account.yaml 支持 phone / payment.credit-card 字段 - data/evidence/(真实 PII)加入 .gitignore Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+106
-21
@@ -15,13 +15,14 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from app.shared.errors import CartOperationError, InvalidRequestError
|
||||
from app.shared.errors import CartOperationError, InvalidRequestError, OrderOperationError
|
||||
from app.trading.worker.models import LeaseTask
|
||||
from app.trading.worker.site_interact import (
|
||||
CheckoutSummary,
|
||||
SiteInteractor,
|
||||
_extract_error_message,
|
||||
_extract_purchase_fields,
|
||||
_parse_checkout_summary,
|
||||
_parse_initial_state,
|
||||
)
|
||||
|
||||
@@ -249,32 +250,116 @@ def test_site_interactor_construction_does_not_require_playwright():
|
||||
assert site._per_task_state == {}
|
||||
|
||||
|
||||
# ---- 未实现的方法仍然抛 NotImplementedError ----
|
||||
# ---- monitor 仍未实现,抛 NotImplementedError ----
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method,args",
|
||||
[
|
||||
("enter_checkout", (_make_task(),)),
|
||||
("submit_order", (_make_task(),)),
|
||||
("pay", (_make_task(), "ord-1")),
|
||||
("monitor", (_make_task(), "ord-1")),
|
||||
],
|
||||
)
|
||||
async def test_unimplemented_methods_raise(method: str, args: tuple):
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
fn = getattr(site, method)
|
||||
with pytest.raises(NotImplementedError):
|
||||
await fn(*args)
|
||||
|
||||
|
||||
async def test_parse_checkout_unimplemented():
|
||||
async def test_monitor_unimplemented():
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
with pytest.raises(NotImplementedError):
|
||||
await site.parse_checkout("<html></html>")
|
||||
await site.monitor(_make_task(), "ord-1")
|
||||
|
||||
|
||||
# ---- add_to_cart / verify_cart 在没启动 Playwright 时应失败 ----
|
||||
# ---- submit_order / pay:没有 enter_checkout 留存的确认页会话时应报错,不静默成功 ----
|
||||
|
||||
|
||||
async def test_submit_order_without_checkout_page_raises():
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
with pytest.raises(OrderOperationError):
|
||||
await site.submit_order(_make_task())
|
||||
|
||||
|
||||
async def test_pay_without_checkout_page_raises():
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
with pytest.raises(OrderOperationError):
|
||||
await site.pay(_make_task(), "ord-1")
|
||||
|
||||
|
||||
# ---- parse_checkout / _parse_checkout_summary:金额解析已用真实确认页 HTML 验证
|
||||
# (2026-08-13,data/evidence/checkout-research-20260811/i-final-state.html),
|
||||
# 订单号/付款期限仍未验证。下面简单 fixture 测的是防御性行为(找不到/矛盾就报错)。
|
||||
|
||||
|
||||
async def test_parse_checkout_delegates_to_pure_function():
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
html = "<div>お支払い金額 1,980円</div>"
|
||||
summary = await site.parse_checkout(html)
|
||||
assert summary.payable_yen == 1980
|
||||
|
||||
|
||||
def test_parse_checkout_summary_extracts_amount_order_id_deadline():
|
||||
"""订单号 fixture 用真实格式(三段数字用「-」连接,见 _ORDER_ID_PATTERN 注释),
|
||||
2026-08-13 之前这里用的是纯猜测的 "AB-123456" 字母前缀格式,拿到真实订单号后
|
||||
已确认不是这个形状。"""
|
||||
html = (
|
||||
"<div>お支払い合計 12,345円</div>"
|
||||
"<div>ご注文番号 306087-20260813-0863947697</div>"
|
||||
"<div>お支払い期限 2026/08/20</div>"
|
||||
)
|
||||
summary = _parse_checkout_summary(html)
|
||||
assert summary.payable_yen == 12345
|
||||
assert summary.site_order_id == "306087-20260813-0863947697"
|
||||
assert summary.pay_deadline == "2026/08/20"
|
||||
|
||||
|
||||
def test_parse_checkout_summary_missing_amount_raises():
|
||||
"""找不到「标签+金额」的明确匹配就报错,不落回猜数字——这个值直接喂给金额守卫"""
|
||||
with pytest.raises(OrderOperationError):
|
||||
_parse_checkout_summary("<div>合計 1,980円</div>") # 标签不在候选列表里
|
||||
|
||||
|
||||
def test_parse_checkout_summary_conflicting_amounts_raises():
|
||||
html = "<div>お支払い金額 1,000円</div><div>お支払い合計 2,000円</div>"
|
||||
with pytest.raises(OrderOperationError):
|
||||
_parse_checkout_summary(html)
|
||||
|
||||
|
||||
def test_parse_checkout_summary_optional_fields_default_none():
|
||||
summary = _parse_checkout_summary("<div>お支払い金額 500円</div>")
|
||||
assert summary.payable_yen == 500
|
||||
assert summary.site_order_id is None
|
||||
assert summary.pay_deadline is None
|
||||
|
||||
|
||||
def test_parse_checkout_summary_matches_real_page_dom_shape():
|
||||
"""真实确认页金额结构:标签纯「支払い金額」(无「お」前缀),数字与「円」
|
||||
分别在独立标签里、中间隔着大段 class 属性,不是「标签+数字+円」紧邻文案
|
||||
(2026-08-13 从真实页面简化摘录的结构,见 _AMOUNT_TAG_BOUNDED_PATTERN 注释)"""
|
||||
html = (
|
||||
'<span class="label--x">支払い金額</span></div>'
|
||||
'<div class="number-display--x">'
|
||||
'<div class="value--x">297</div>'
|
||||
'<span class="suffix--x"><div class="text-display--x">円</div></span>'
|
||||
"</div>"
|
||||
)
|
||||
summary = _parse_checkout_summary(html)
|
||||
assert summary.payable_yen == 297
|
||||
|
||||
|
||||
def test_parse_checkout_summary_matches_real_order_id_dom_shape():
|
||||
"""真实下单完成页订单号结构:标签和号码之间是 ` ` 实体而不是普通空白,
|
||||
号码是三段数字用「-」连接(2026-08-13 真实点击「注文を確定する」后从
|
||||
完成页摘录的结构,见 _ORDER_ID_PATTERN 注释)。旧版正则的分隔符只认
|
||||
非单词字符,会卡在 ` ` 里的 n/b/s/p 上、完全连不到号码。"""
|
||||
html = (
|
||||
'<div>お支払い金額 297円</div>'
|
||||
'<span class="label--x">注文番号 306087-20260813-0863947697</span>'
|
||||
)
|
||||
summary = _parse_checkout_summary(html)
|
||||
assert summary.site_order_id == "306087-20260813-0863947697"
|
||||
|
||||
|
||||
# ---- enter_checkout / add_to_cart / verify_cart 在没启动 Playwright 时应失败 ----
|
||||
|
||||
|
||||
async def test_enter_checkout_without_start_raises():
|
||||
"""没调 start() 就调 enter_checkout,auth_session=None,应在 require_logged_in 处抛错
|
||||
|
||||
与 add_to_cart 同理:测的是「未启动时不应该静默成功」,不是 CheckoutBlockedError
|
||||
这条业务分支(那条分支需要真实 Playwright page,不在离线单测范围)。
|
||||
"""
|
||||
site = SiteInteractor(auth_session=None, settings=None) # type: ignore[arg-type]
|
||||
with pytest.raises((AttributeError, TypeError)):
|
||||
await site.enter_checkout(_make_task())
|
||||
|
||||
|
||||
async def test_add_to_cart_without_start_raises():
|
||||
|
||||
Reference in New Issue
Block a user