Files
workphone-sdk/sdk/tests/test_wechat_payment_headless_contract.py

244 lines
8.2 KiB
Python

"""WP-HL-06 支付消息测试号门禁契约。
只验证 HTTP/BFF 调用进入无线 Frida RPC 前后的契约:不连接设备,
不发起任何真实资金动作。
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from typing import Any, Callable
import pytest
APP = Path(__file__).resolve().parents[1] / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from routers import unified
TEST_DEVICE = "payment-fixture-device"
TEST_TARGET = "payment-fixture-target"
@pytest.fixture(autouse=True)
def payment_test_gate(monkeypatch):
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", TEST_TARGET)
monkeypatch.setenv("WP_PAYMENT_MAX_TEST_AMOUNT", "0.10")
monkeypatch.setenv("WP_PAYMENT_RATE_WINDOW_SECONDS", "60")
monkeypatch.setenv("WP_PAYMENT_RATE_MAX_CALLS", "2")
unified._payment_rate_events.clear()
def _request(action: str, *, real: bool = False, target: str = TEST_TARGET):
flags = {"confirm": real, "dry_run": not real}
if action in {"payment_receive", "receive_payment"}:
return unified.ReceivePaymentRequest(
device_id=TEST_DEVICE,
platform="wechat",
payer_id=target,
amount="0.01",
desc="测试收款",
**flags,
)
if action == "receive_red_packet":
return unified.RedPacketReceiveRequest(
device_id=TEST_DEVICE,
platform="wechat",
from_id=target,
msg_svr_id="fixture-red-packet-msg",
**flags,
)
if action == "receive_transfer":
return unified.TransferDecisionRequest(
device_id=TEST_DEVICE,
platform="wechat",
from_id=target,
msg_svr_id="fixture-transfer-msg",
**flags,
)
if action == "send_red_packet":
return unified.RedPacketRequest(
device_id=TEST_DEVICE,
platform="wechat",
to_id=target,
amount="0.01",
**flags,
)
if action in {"send_transfer", "transfer"}:
return unified.TransferRequest(
device_id=TEST_DEVICE,
platform="wechat",
to_id=target,
amount="0.01",
payment_password="fixture-password",
**flags,
)
raise AssertionError(f"missing fixture for {action}")
PAYMENT_VECTORS: list[tuple[str, Callable[[Any], Any], str]] = [
("payment_receive", unified.receive_payment, "receive_payment"),
("receive_payment", unified.receive_payment, "receive_payment"),
("receive_red_packet", unified.receive_red_packet, "receive_red_packet"),
("receive_transfer", unified.receive_transfer_payment, "receive_transfer"),
("send_red_packet", unified.send_red_packet, "send_red_packet"),
("send_transfer", unified.send_transfer, "send_transfer"),
("transfer", unified.send_transfer, "send_transfer"),
]
@pytest.mark.parametrize(("action", "handler", "rpc_action"), PAYMENT_VECTORS)
def test_all_payment_actions_default_to_confirmed_dry_run(action, handler, rpc_action):
response = asyncio.run(handler(_request(action)))
assert response["code"] == 200
assert response["success"] is True
assert response["channel_used"] == "dry_run"
assert response["data"]["dry_run"] is True
assert response["data"]["confirm_required"] is True
assert response["data"]["action"] == rpc_action
@pytest.mark.parametrize(("action", "handler", "rpc_action"), PAYMENT_VECTORS)
def test_all_payment_actions_reject_non_fixture_target_before_device_call(
monkeypatch, action, handler, rpc_action
):
monkeypatch.setattr(
unified,
"_check_device_online",
lambda *args, **kwargs: pytest.fail("测试对象门禁不应触达设备"),
)
response = asyncio.run(handler(_request(action, real=True, target="outside-fixture")))
assert response["code"] == 403
assert response["success"] is False
assert response["data"]["action"] == rpc_action
assert response["data"]["error_code"] == "payment_test_target_required"
@pytest.mark.parametrize(
("action", "handler"),
[
("payment_receive", unified.receive_payment),
("send_red_packet", unified.send_red_packet),
("send_transfer", unified.send_transfer),
],
)
def test_amount_guard_rejects_over_fixture_limit(action, handler):
request = _request(action, real=True)
request.amount = "0.11"
response = asyncio.run(handler(request))
assert response["code"] == 422
assert response["success"] is False
assert response["data"]["error_code"] == "payment_amount_out_of_range"
def test_payment_frequency_gate_returns_retry_after_seconds():
assert unified._payment_guard(
device_id=TEST_DEVICE,
action="receive_red_packet",
target_id=TEST_TARGET,
) is None
assert unified._payment_guard(
device_id=TEST_DEVICE,
action="receive_red_packet",
target_id=TEST_TARGET,
) is None
blocked = unified._payment_guard(
device_id=TEST_DEVICE,
action="receive_red_packet",
target_id=TEST_TARGET,
)
assert blocked is not None
assert blocked["code"] == 429
assert blocked["data"]["error_code"] == "rate_limited"
assert blocked["data"]["retry_after_seconds"] >= 1
@pytest.mark.parametrize(("action", "handler", "rpc_action"), PAYMENT_VECTORS)
def test_real_payment_path_requires_frida_receipt_trace_and_readback(
monkeypatch, action, handler, rpc_action
):
captured: dict[str, Any] = {}
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def fake_execute(device_id, platform, hook_action, params, **kwargs):
captured.update(device_id=device_id, platform=platform, action=hook_action, params=params, kwargs=kwargs)
return {
"code": 200,
"success": True,
"verified": True,
"channel_used": "frida_rpc",
"trace_id": f"trace-{rpc_action}",
"raw_rpc_receipt": {"rpc_action": rpc_action, "ok": True},
"readback": {"found": True, "message_id": f"fixture-{rpc_action}"},
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
response = asyncio.run(handler(_request(action, real=True)))
assert captured["action"] == rpc_action
assert response["code"] == 200
assert response["success"] is True
assert response["channel_used"] == "frida_rpc"
assert response["data"]["trace_id"] == f"trace-{rpc_action}"
assert response["data"]["raw_rpc_receipt"]["ok"] is True
assert response["data"]["readback"]["found"] is True
def test_transfer_secret_is_masked_and_missing_secret_blocks_rpc(monkeypatch):
dry_response = asyncio.run(unified.send_transfer(_request("transfer")))
assert dry_response["data"]["password_present"] is True
assert "fixture-password" not in str(dry_response)
monkeypatch.setattr(
unified,
"_check_device_online",
lambda *args, **kwargs: pytest.fail("缺少密码门禁不应触达设备"),
)
response = asyncio.run(
unified.send_transfer(
unified.TransferRequest(
device_id=TEST_DEVICE,
platform="wechat",
to_id=TEST_TARGET,
amount="0.01",
confirm=True,
dry_run=False,
)
)
)
assert response["code"] == 422
assert response["data"]["error_code"] == "payment_password_required"
def test_payment_actions_have_explicit_api_routes_and_no_u2_fallback():
from services.device_transport import WECHAT_U2_ONLY_ACTIONS
from main import app
paths = app.openapi()["paths"]
assert {
"/api/v3/payment/red-packet",
"/api/v3/payment/transfer",
"/api/v3/payment/transfer/receive",
"/api/v3/payment/receive-red-packet",
"/api/v3/payment/receive",
"/api/v3/payment/receive-payment",
"/api/v3/payment/payment-receive",
}.issubset(paths)
assert not {
"payment_receive",
"receive_payment",
"receive_red_packet",
"receive_transfer",
"send_red_packet",
"send_transfer",
"transfer",
}.intersection(WECHAT_U2_ONLY_ACTIONS)