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

1135 lines
39 KiB
Python

"""消息管理九路由契约回归。
本文件只做离线契约测试:不连接真机、不执行真实消息写操作。
"""
from __future__ import annotations
import asyncio
import sys
from pathlib import Path
from typing import Any
import pytest
from fastapi.routing import APIRoute
from pydantic import BaseModel, ValidationError
APP = Path(__file__).resolve().parents[1] / "app"
if str(APP) not in sys.path:
sys.path.insert(0, str(APP))
from routers import unified
@pytest.fixture(autouse=True)
def payment_test_gate(monkeypatch):
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "filehelper,卡若,udbfnvtk,test-user")
unified._payment_rate_events.clear()
MESSAGE_ROUTES = {
"/message/send": unified.SendMessageRequest,
"/message/list": unified.GetMessagesRequest,
"/message/sync-since": unified.MessageSyncSinceRequest,
"/message/batch-send": unified.BatchSendMessageRequest,
"/comment/reply": unified.ReplyCommentRequest,
"/message/forward": unified.ForwardMessageRequest,
"/message/recall": unified.RecallMessageRequest,
"/message/send-card": unified.SendCardRequest,
"/message/voice": unified.SendVoiceMessageRequest,
}
REQUIRED_FIELDS = {
unified.SendMessageRequest: {"device_id", "platform", "to_id", "content"},
unified.GetMessagesRequest: {"device_id", "platform"},
unified.MessageSyncSinceRequest: {"device_id"},
unified.BatchSendMessageRequest: {"device_id", "platform", "to_ids", "content"},
unified.ReplyCommentRequest: {"device_id", "platform", "comment_id", "content"},
unified.ForwardMessageRequest: {"device_id", "platform", "to_id"},
unified.RecallMessageRequest: {"device_id"},
unified.SendCardRequest: {"device_id", "platform", "to_id", "card_wxid"},
unified.SendVoiceMessageRequest: {"device_id"},
}
VALID_REQUESTS = {
unified.SendMessageRequest: {
"device_id": "device-1",
"platform": "wechat",
"to_id": "filehelper",
"content": "hello",
},
unified.GetMessagesRequest: {"device_id": "device-1", "platform": "wechat"},
unified.MessageSyncSinceRequest: {"device_id": "device-1"},
unified.BatchSendMessageRequest: {
"device_id": "device-1",
"platform": "wechat",
"to_ids": ["filehelper"],
"content": "hello",
},
unified.ReplyCommentRequest: {
"device_id": "device-1",
"platform": "douyin",
"comment_id": "comment-1",
"content": "reply",
},
unified.ForwardMessageRequest: {
"device_id": "device-1",
"platform": "wechat",
"to_id": "filehelper",
"msg_svr_id": "10001",
},
unified.RecallMessageRequest: {
"device_id": "device-1",
"platform": "wechat",
"msg_svr_id": "10001",
},
unified.SendCardRequest: {
"device_id": "device-1",
"platform": "wechat",
"to_id": "filehelper",
"card_wxid": "wxid_card",
},
unified.SendVoiceMessageRequest: {
"device_id": "device-1",
"platform": "wechat",
"to_id": "filehelper",
},
}
def _route(path: str) -> APIRoute:
matches = [
route
for route in unified.router.routes
if isinstance(route, APIRoute)
and route.path == path
and "POST" in route.methods
]
assert len(matches) == 1, f"{path} 应且仅应注册一个POST路由"
return matches[0]
def _assert_invalid(model: type[BaseModel], patch: dict[str, Any]) -> None:
payload = dict(VALID_REQUESTS[model])
payload.update(patch)
with pytest.raises(ValidationError):
model.model_validate(payload)
@pytest.mark.parametrize("path", MESSAGE_ROUTES)
def test_nine_routes_have_explicit_response_model_and_error_contract(path: str):
route = _route(path)
assert route.response_model not in (None, dict)
assert {400, 401, 422, 503}.issubset(set(route.responses))
for status in (400, 401, 422, 503):
assert route.responses[status].get("description")
@pytest.mark.parametrize("model,required", REQUIRED_FIELDS.items())
def test_request_models_publish_required_fields_and_accept_valid_payload(
model: type[BaseModel], required: set[str]
):
fields = model.model_fields
assert required.issubset(fields)
assert {name for name, field in fields.items() if field.is_required()} == required
assert model.model_validate(VALID_REQUESTS[model])
@pytest.mark.parametrize(
("model", "patch"),
[
(unified.SendMessageRequest, {"device_id": ""}),
(unified.SendMessageRequest, {"to_id": ""}),
(unified.SendMessageRequest, {"content": ""}),
(unified.SendMessageRequest, {"timeout_seconds": 4}),
(unified.SendMessageRequest, {"timeout_seconds": 181}),
(unified.GetMessagesRequest, {"limit": 0}),
(unified.GetMessagesRequest, {"limit": unified.MAX_MESSAGE_PULL_LIMIT + 1}),
(unified.GetMessagesRequest, {"offset": -1}),
(unified.GetMessagesRequest, {"since_time": -1}),
(unified.MessageSyncSinceRequest, {"since_time": -1}),
(unified.MessageSyncSinceRequest, {"limit": 0}),
(unified.MessageSyncSinceRequest, {"limit": unified.MAX_MESSAGE_PULL_LIMIT + 1}),
(unified.MessageSyncSinceRequest, {"offset": -1}),
(unified.BatchSendMessageRequest, {"to_ids": []}),
(unified.BatchSendMessageRequest, {"to_ids": ["x"] * 101}),
(unified.BatchSendMessageRequest, {"content": ""}),
(unified.BatchSendMessageRequest, {"interval": 0.4}),
(unified.BatchSendMessageRequest, {"interval": 31}),
(unified.ReplyCommentRequest, {"comment_id": ""}),
(unified.ReplyCommentRequest, {"content": ""}),
(unified.ForwardMessageRequest, {"to_id": ""}),
(unified.RecallMessageRequest, {"device_id": ""}),
(unified.SendCardRequest, {"to_id": ""}),
(unified.SendCardRequest, {"card_wxid": ""}),
(unified.SendVoiceMessageRequest, {"device_id": ""}),
(unified.SendVoiceMessageRequest, {"duration": 0}),
(unified.SendVoiceMessageRequest, {"duration": 61}),
],
)
def test_request_model_constraints_reject_invalid_values(
model: type[BaseModel], patch: dict[str, Any]
):
_assert_invalid(model, patch)
def test_message_error_passthrough_helper_preserves_original_semantics():
response = unified._message_operation_response(
{
"code": 503,
"success": False,
"error_code": "frida_receiver_missing",
"error_message": "no_receiver_registered",
"retryable": True,
"retry_after_seconds": 12,
"channel_used": "hook",
"trace_id": "trace-message-contract-001",
"raw_rpc_receipt": {"rpc": "sendMessage", "implemented": False},
"readback": None,
},
action="send_message",
target_id="filehelper",
require_message_id=True,
)
assert response["code"] == 503
assert response["success"] is False
assert response["channel_used"] == "hook"
assert response["data"]["error_code"] == "frida_receiver_missing"
assert response["data"]["error_message"] == "no_receiver_registered"
assert response["data"]["retryable"] is True
assert response["data"]["retry_after_seconds"] == 12
assert response["trace_id"] == "trace-message-contract-001"
assert response["data"]["trace_id"] == "trace-message-contract-001"
assert response["data"]["raw_rpc_receipt"] == {"rpc": "sendMessage", "implemented": False}
assert response["data"]["readback"] is None
assert response["feedback"]["message"] == "no_receiver_registered"
def test_message_response_does_not_generate_trace_when_execution_lacks_evidence():
response = unified._message_operation_response(
{"code": 503, "success": False, "error_code": "hook_unavailable"},
action="send_message",
target_id="filehelper",
require_message_id=True,
)
assert response["trace_id"] == ""
assert response["data"]["raw_rpc_receipt"] is None
assert response["data"]["readback"] is None
def test_sync_since_is_strictly_incremental_sorted_and_deduplicated(monkeypatch):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def fake_execute(*args, **kwargs):
return {
"code": 200,
"_channel_used": "hook",
"data": {
"success": True,
"messages": [
{"message_id": "m2", "timestamp": 102, "content": "second"},
{"message_id": "boundary", "timestamp": 100, "content": "equal"},
{"message_id": "m1", "timestamp": 101, "content": "first"},
{"message_id": "m1", "timestamp": 101, "content": "duplicate"},
{"message_id": "old", "timestamp": 99, "content": "old"},
],
},
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
response = asyncio.run(
unified.sync_messages_since(
unified.MessageSyncSinceRequest(
device_id="device-1",
platform="wechat",
since_time=100,
limit=10,
)
)
)
messages = response["data"]["messages"]
assert [item["message_id"] for item in messages] == ["m1", "m2"]
assert all(item["timestamp"] > 100 for item in messages)
assert response["data"]["count"] == 2
assert response["data"]["since_time"] == 100
assert response["data"]["next_since_time"] == 102
assert response["channel_used"] == "hook"
def test_ai_channel_does_not_generate_fake_message_id(monkeypatch):
async def pass_guard(*args, **kwargs):
return {"pass": True, "content": "hello"}
async def fake_agent(*args, **kwargs):
return {"success": True, "channel_used": "ai_agent"}
async def no_log(*args, **kwargs):
return None
monkeypatch.setattr(unified, "_anti_ban_guard", pass_guard)
monkeypatch.setattr(unified, "_get_device_mode", lambda *args, **kwargs: "offline")
monkeypatch.setattr(unified, "_send_via_agent", fake_agent)
monkeypatch.setattr(unified.device_manager, "log_command", no_log)
monkeypatch.setattr(
unified.rate_limiter, "trip_silent_throttle", lambda *args, **kwargs: 60
)
response = asyncio.run(
unified.send_message(
unified.SendMessageRequest(
device_id="device-1",
platform="wechat",
to_id="filehelper",
content="hello",
)
)
)
assert response["channel_used"] == "ai_agent"
assert response["data"]["message_id"] is None
assert response["data"]["verified"] is False
assert response["data"]["status"] == "accepted_unverified"
@pytest.mark.parametrize(
("handler", "request_obj", "action"),
[
(
unified.forward_message,
unified.ForwardMessageRequest(
device_id="device-1",
platform="wechat",
to_id="filehelper",
),
"forward_message",
),
(
unified.recall_message,
unified.RecallMessageRequest(device_id="device-1", platform="wechat"),
"recall_message",
),
],
)
def test_wechat_forward_and_recall_require_msg_svr_id(
monkeypatch, handler, request_obj, action
):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def should_not_execute(*args, **kwargs):
pytest.fail("参数校验失败时不应下发设备动作")
monkeypatch.setattr(unified, "_execute_skill", should_not_execute)
response = asyncio.run(handler(request_obj))
assert response["code"] == 422
assert response["success"] is False
assert response["data"]["action"] == action
assert response["data"]["status"] == "validation_failed"
assert response["data"]["error_code"] == "validation_failed"
assert response["channel_used"] == "none"
def test_voice_rejects_empty_target_before_device_execution(monkeypatch):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def should_not_execute(*args, **kwargs):
pytest.fail("空目标时不应下发设备动作")
monkeypatch.setattr(unified, "_execute_skill", should_not_execute)
response = asyncio.run(
unified.send_voice_message(
unified.SendVoiceMessageRequest(
device_id="device-1",
platform="wechat",
to_id=" ",
user_id=" ",
)
)
)
assert response["code"] == 422
assert response["success"] is False
assert response["data"]["status"] == "validation_failed"
assert response["data"]["error_code"] == "validation_failed"
assert response["channel_used"] == "none"
def test_batch_guard_returns_one_failure_item_per_target(monkeypatch):
async def blocked_guard(*args, **kwargs):
return {"pass": False, "reason": "新号期禁止高风险操作: batch_send"}
monkeypatch.setattr(unified, "_anti_ban_guard", blocked_guard)
response = asyncio.run(
unified.batch_send_message(
unified.BatchSendMessageRequest(
device_id="device-1",
platform="wechat",
to_ids=["filehelper", "wxid_target"],
content="batch test",
)
)
)
assert response["code"] == 503
assert response["data"]["failed_count"] == 2
assert [item["to_id"] for item in response["data"]["failed"]] == [
"filehelper",
"wxid_target",
]
assert all(
item["error_code"] == "rate_limited"
for item in response["data"]["failed"]
)
def test_send_red_packet_defaults_to_verified_dry_run(monkeypatch):
monkeypatch.setattr(
unified,
"_check_device_online",
lambda *args, **kwargs: pytest.fail("演练模式不应连接设备"),
)
response = asyncio.run(
unified.send_red_packet(
unified.RedPacketRequest(
device_id="device-1",
platform="wechat",
to_id="filehelper",
amount="0.01",
)
)
)
assert response["code"] == 200
assert response["success"] is True
assert response["data"]["verified"] is True
assert response["data"]["dry_run"] is True
assert response["data"]["confirm_required"] is True
assert response["channel_used"] == "dry_run"
def test_red_packet_real_failure_is_not_wrapped_as_success(monkeypatch):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def failed_execute(*args, **kwargs):
return {
"code": 200,
"success": False,
"error": "no_receiver_registered",
"_channel_used": "server/frida",
}
monkeypatch.setattr(unified, "_execute_skill", failed_execute)
response = asyncio.run(
unified.send_red_packet(
unified.RedPacketRequest(
device_id="device-1",
platform="wechat",
to_id="filehelper",
amount="0.01",
confirm=True,
dry_run=False,
)
)
)
assert response["code"] == 503
assert response["success"] is False
assert response["data"]["error_message"] == "no_receiver_registered"
assert response["channel_used"] == "server/frida"
def test_send_red_packet_real_path_checks_hook_action(monkeypatch):
checked = {}
def fake_check(device_id, platform="wechat", action=""):
checked.update(device_id=device_id, platform=platform, action=action)
return True
async def fake_execute(*args, **kwargs):
return {
"code": 200,
"success": True,
"verified": True,
"payment_status": "prepared_waiting_pay_confirm",
"trace_id": "trace-send-red-packet",
"raw_rpc_receipt": {"err_type": 0, "err_code": 0},
"readback": {"message_id": "msg-red-packet"},
"_channel_used": "server/frida",
}
monkeypatch.setattr(unified, "_check_device_online", fake_check)
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
response = asyncio.run(
unified.send_red_packet(
unified.RedPacketRequest(
device_id="device-1",
platform="wechat",
to_id="filehelper",
amount="0.01",
confirm=True,
dry_run=False,
)
)
)
assert checked["action"] == "send_red_packet"
assert response["code"] == 200
assert response["channel_used"] == "server/frida"
def test_receive_red_packet_defaults_to_verified_dry_run(monkeypatch):
monkeypatch.setattr(
unified,
"_check_device_online",
lambda *args, **kwargs: pytest.fail("演练模式不应连接设备"),
)
response = asyncio.run(
unified.receive_red_packet(
unified.RedPacketReceiveRequest(
device_id="device-1",
platform="wechat",
msg_svr_id="msg-1",
from_id="卡若",
)
)
)
assert response["code"] == 200
assert response["success"] is True
assert response["data"]["dry_run"] is True
assert response["data"]["confirm_required"] is True
assert response["data"]["msg_svr_id"] == "msg-1"
assert response["channel_used"] == "dry_run"
def test_receive_red_packet_real_path_uses_msg_svr_id(monkeypatch):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
called = {}
async def fake_execute(device_id, platform, action, params, **kwargs):
called.update(
device_id=device_id,
platform=platform,
action=action,
params=params,
)
return {
"code": 200,
"success": True,
"verified": True,
"status": "received",
"msg_svr_id": params["msg_svr_id"],
"trace_id": "trace-receive-red-packet",
"raw_rpc_receipt": {"err_type": 0, "err_code": 0},
"readback": {"msg_svr_id": params["msg_svr_id"]},
"_channel_used": "server/frida",
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
response = asyncio.run(
unified.receive_red_packet(
unified.RedPacketReceiveRequest(
device_id="device-1",
platform="wechat",
msg_svr_id="10435",
from_id="卡若",
confirm=True,
dry_run=False,
)
)
)
assert called["action"] == "receive_red_packet"
assert called["params"]["msg_svr_id"] == "10435"
assert response["code"] == 200
assert response["success"] is True
assert response["channel_used"] == "server/frida"
@pytest.mark.parametrize(
("handler", "action"),
[
(unified.receive_transfer_payment, "receive_transfer"),
(unified.reject_transfer_payment, "reject_transfer"),
],
)
def test_transfer_decision_defaults_to_verified_dry_run(
monkeypatch, handler, action
):
monkeypatch.setattr(
unified,
"_check_device_online",
lambda *args, **kwargs: pytest.fail("演练模式不应连接设备"),
)
response = asyncio.run(
handler(
unified.TransferDecisionRequest(
device_id="device-1",
platform="wechat",
msg_svr_id="10436",
from_id="卡若",
)
)
)
assert response["code"] == 200
assert response["success"] is True
assert response["data"]["action"] == action
assert response["data"]["verified"] is True
assert response["data"]["dry_run"] is True
assert response["channel_used"] == "dry_run"
def test_reject_transfer_confirmed_result_uses_hook_only_channel(monkeypatch):
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: True)
async def fake_execute(device_id, platform, action, params, **kwargs):
assert action == "reject_transfer"
assert params["confirm"] is True
assert kwargs["hook_only"] is True
return {
"success": True,
"verified": True,
"status": "rejected",
"msg_svr_id": "10436",
"trace_id": "trace-reject-transfer",
"raw_rpc_receipt": {"err_type": 0, "err_code": 0},
"readback": {"msg_svr_id": "10436", "pay_subtype": "3"},
"_channel_used": "server/frida",
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
response = asyncio.run(
unified.reject_transfer_payment(
unified.TransferDecisionRequest(
device_id="device-1",
platform="wechat",
msg_svr_id="10436",
from_id="卡若",
confirm=True,
dry_run=False,
)
)
)
assert response["code"] == 200
assert response["success"] is True
assert response["data"]["status"] == "verified"
assert response["data"]["operation_status"] == "rejected"
assert response["channel_used"] == "server/frida"
def test_transfer_decisions_are_not_ui_only_actions():
from services.device_transport import WECHAT_U2_ONLY_ACTIONS
assert "send_red_packet" not in WECHAT_U2_ONLY_ACTIONS
assert "receive_red_packet" not in WECHAT_U2_ONLY_ACTIONS
assert "receive_transfer" not in WECHAT_U2_ONLY_ACTIONS
assert "reject_transfer" not in WECHAT_U2_ONLY_ACTIONS
def test_check_device_online_allows_server_frida_when_ws_offline(monkeypatch):
from services.device_transport import device_transport
monkeypatch.setattr(device_transport, "resolve_mode", lambda *args, **kwargs: "offline")
class DummyBridge:
@staticmethod
def enabled_for(device_id):
return device_id == "device-1"
import services.server_frida_bridge as bridge_module
monkeypatch.setattr(bridge_module, "server_frida_bridge", DummyBridge())
assert (
device_transport.check_device_online("device-1", "wechat", "receive_red_packet")
== "hook"
)
def test_transfer_hook_rpc_uses_internal_network_scene_only():
hook_path = (
Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
)
source = hook_path.read_text(encoding="utf-8")
start = source.index("function _executeTransferOperation")
end = source.index("// ============================================================", start)
implementation = source[start:end]
assert "com.tencent.mm.plugin.remittance.model.n0" in implementation
assert "/cgi-bin/mmpay-bin/transferoperation" in implementation
assert "cgi_func_id: 1691" in implementation
assert "_intentAction" not in implementation
assert "input tap" not in implementation
def test_red_packet_receive_hook_uses_internal_network_scenes_only():
hook_path = (
Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
)
source = hook_path.read_text(encoding="utf-8")
start = source.index("function _executeRedPacketReceive")
end = source.index("function _executeTransferOperation", start)
implementation = source[start:end]
assert "com.tencent.mm.plugin.luckymoney.model.u5" in implementation
assert "com.tencent.mm.plugin.luckymoney.model.o5" in implementation
assert "/cgi-bin/mmpay-bin/receivewxhb" in implementation
assert "/cgi-bin/mmpay-bin/openwxhb" in implementation
assert "cgi_func_id: 1581" in implementation
assert "cgi_func_id: 1685" in implementation
assert "_intentAction" not in implementation
assert "input tap" not in implementation
def test_red_packet_send_hook_uses_internal_prepare_scene_only():
hook_path = (
Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
)
source = hook_path.read_text(encoding="utf-8")
start = source.index("function _executeRedPacketPrepareSend")
end = source.index("function _executeTransferOperation", start)
implementation = source[start:end]
assert "com.tencent.mm.plugin.luckymoney.model.q5" in implementation
assert "/cgi-bin/mmpay-bin/requestwxhb" in implementation
assert "cgi_func_id: 1575" in implementation
assert "reqkey" in implementation
assert "send_msg_xml" in implementation
assert "_intentAction" not in implementation
assert "input tap" not in implementation
def test_hook_copies_are_identical_after_red_packet_update():
root = Path(__file__).resolve().parents[1]
canonical = (root / "agent" / "hook" / "wechat_hook_v2.js").read_bytes()
assert (root / "app" / "agent" / "hook" / "wechat_hook_v2.js").read_bytes() == canonical
assert (
root
/ "android-app"
/ "app"
/ "src"
/ "main"
/ "assets"
/ "wechat_hook_v2.js"
).read_bytes() == canonical
def test_message_list_online_check_uses_get_messages_action():
router_path = Path(__file__).resolve().parents[1] / "app" / "routers" / "unified.py"
source = router_path.read_text(encoding="utf-8")
start = source.index("async def get_messages(req: GetMessagesRequest)")
end = source.index('@router.post(\n "/message/sync-since"', start)
implementation = source[start:end]
assert '_check_device_online(req.device_id, req.platform.value, "get_messages")' in implementation
assert '"send_red_packet"' not in implementation
def test_incoming_payment_batch_reports_each_attempt_and_summary():
router_path = Path(__file__).resolve().parents[1] / "app" / "routers" / "unified.py"
source = router_path.read_text(encoding="utf-8")
start = source.index("async def receive_incoming_payment_batch")
end = source.index("# =============================================================================", start)
implementation = source[start:end]
assert '"receive_transfer"' in implementation
assert '"receive_red_packet"' in implementation
assert '"requested_count"' in implementation
assert '"success_count"' in implementation
assert '"failed_count"' in implementation
assert '"skipped_count"' in implementation
def test_wp_hl_06_payment_routes_and_aliases_registered():
from main import app
paths = app.openapi()["paths"]
expected = {
"/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",
"/api/v3/payment/receive-incoming-batch",
}
assert expected.issubset(set(paths))
def test_wp_hl_06_transfer_defaults_to_dry_run_confirm_gate():
import asyncio
from routers import unified
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(device_id="device-1", to_id="test-user", amount="0.01")
))
assert resp["success"] is True
assert resp["channel_used"] == "dry_run"
assert resp["data"]["dry_run"] is True
assert resp["data"]["confirm_required"] is True
def test_wp_hl_06_transfer_accepts_payment_password_without_echo():
import asyncio
from routers import unified
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-1",
to_id="test-user",
amount="0.01",
payment_password="123456",
)
))
text = str(resp)
assert resp["success"] is True
assert resp["data"]["password_present"] is True
assert "123456" not in text
def test_wp_hl_06_transfer_forwards_payment_password_masked(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: "hook")
captured = {}
async def fake_execute(device_id, platform, action, params, **kwargs):
captured.update(params)
return {
"code": 202,
"success": False,
"verified": True,
"channel_used": "frida_rpc",
"trace_id": "trace-password",
"raw_rpc_receipt": {"ok": True},
"prepared": True,
"payment_confirm_required": True,
"status": "payment_confirm_required",
"error_code": "payment_confirm_required",
"readback": {"found": False},
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-password",
to_id="test-user",
amount="0.01",
pay_password="123456",
confirm=True,
dry_run=False,
)
))
assert captured["payment_password"] == "123456"
assert captured["password_present"] is True
assert resp["code"] == 202
assert "123456" not in str(resp)
def test_wp_hl_06_send_transfer_requires_password_before_rpc(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should not touch device")))
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-password-required",
to_id="test-user",
amount="0.01",
confirm=True,
dry_run=False,
)
))
assert resp["success"] is False
assert resp["code"] == 422
assert resp["data"]["error_code"] == "payment_password_required"
def test_wp_hl_06_payment_amount_guard_rejects_large_real_action(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-1",
to_id="test-user",
amount="2.01",
confirm=True,
dry_run=False,
)
))
assert resp["success"] is False
assert resp["data"]["error_code"] == "payment_amount_out_of_range"
def test_wp_hl_06_payment_target_guard_requires_whitelist(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
resp = asyncio.run(unified.send_red_packet(
unified.RedPacketRequest(
device_id="device-1",
platform=unified.Platform.WECHAT,
to_id="not-in-whitelist",
amount="0.01",
confirm=True,
dry_run=False,
)
))
assert resp["success"] is False
assert resp["code"] == 403
assert resp["data"]["error_code"] == "payment_test_target_required"
def test_wp_hl_06_payment_rate_limit_blocks_fourth_real_action(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
monkeypatch.setenv("WP_PAYMENT_RATE_MAX_CALLS", "3")
monkeypatch.setenv("WP_PAYMENT_RATE_WINDOW_SECONDS", "60")
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: "hook")
async def fake_execute(*args, **kwargs):
return {
"code": 200,
"success": True,
"verified": True,
"channel_used": "frida_rpc",
"trace_id": "trace-rate",
"raw_rpc_receipt": {"ok": True},
"readback": {"ok": True},
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
for _ in range(3):
ok = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-rate",
to_id="test-user",
amount="0.01",
payment_password="123456",
confirm=True,
dry_run=False,
)
))
assert ok["success"] is True
blocked = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-rate",
to_id="test-user",
amount="0.01",
payment_password="123456",
confirm=True,
dry_run=False,
)
))
assert blocked["success"] is False
assert blocked["code"] == 429
assert blocked["data"]["error_code"] == "rate_limited"
def test_wp_hl_06_payment_success_requires_trace_raw_receipt_and_readback(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: "hook")
async def fake_execute(*args, **kwargs):
return {
"code": 200,
"success": True,
"verified": True,
"channel_used": "frida_rpc",
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
resp = asyncio.run(unified.send_transfer(
unified.TransferRequest(
device_id="device-evidence",
to_id="test-user",
amount="0.01",
payment_password="123456",
confirm=True,
dry_run=False,
)
))
assert resp["success"] is False
assert resp["code"] == 503
assert resp["data"]["error_code"] == "payment_readback_missing"
assert set(resp["data"]["missing_evidence"]) >= {"trace_id", "raw_rpc_receipt", "message_or_ledger_readback"}
def test_wp_hl_06_payment_actions_have_no_u2_fallback():
from services.device_transport import WECHAT_U2_ONLY_ACTIONS
for action in [
"payment_receive",
"receive_payment",
"receive_red_packet",
"receive_transfer",
"send_red_packet",
"send_transfer",
"transfer",
]:
assert action not in WECHAT_U2_ONLY_ACTIONS
def test_wp_hl_06_transfer_hook_has_kinda_password_autofill_path():
hook_path = Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
source = hook_path.read_text(encoding="utf-8")
assert "_startKindaSnsTransferPay" in source
assert "KindaPwdInputViewImpl" in source
assert "setOnEndEnterPasswordCallback" in source
assert "inspectKindaPaymentAutofill" in source
assert "kinda_autofill_attempted" in source
def test_wp_hl_06_payment_hook_capabilities_are_explicit_frida_status():
hook_path = Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
source = hook_path.read_text(encoding="utf-8")
send_start = source.index("sendTransfer: function")
send_end = source.index("getWalletBalance: function", send_start)
receive_start = source.index("receivePayment: function", send_end)
receive_end = source.index("autoRegister: function", receive_start)
send_impl = source[send_start:send_end]
receive_impl = source[receive_start:receive_end]
assert "_executeTransferPrepareSend" in source
assert "wechat_internal_transfer_prepare" in send_impl
assert "send_transfer_scene_constructor_unresolved" in source
assert "com.tencent.mm.plugin.remittance.model.v" in source
assert "model_v_int_8strings_scene_user_fee_desc_feeType_receiver_request_ext" in source
assert "/cgi-bin/mmpay-bin/transfer" in source
assert "receive_payment_internal_rpc_not_ready" in receive_impl
assert "capability_unavailable" in receive_impl
assert "frida_rpc" in send_impl
assert "frida_rpc" in receive_impl
assert "_intentAction" not in send_impl
assert "_intentAction" not in receive_impl
def test_wp_hl_06_transfer_confirm_endpoint_dry_run_masks_password():
import asyncio
from routers import unified
resp = asyncio.run(unified.confirm_prepared_transfer(
unified.TransferConfirmRequest(
device_id="device-1",
req_key="sns_tf_test_req_key",
to_id="test-user",
payment_password="123456",
)
))
assert resp["success"] is True
assert resp["channel_used"] == "dry_run"
assert resp["data"]["confirm_required"] is True
assert resp["data"]["password_present"] is True
assert "123456" not in str(resp)
def test_wp_hl_06_transfer_confirm_forwards_to_hook_without_echo(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setenv("WP_PAYMENT_TEST_TARGETS", "test-user")
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: "hook")
captured = {}
async def fake_execute(device_id, platform, action, params, **kwargs):
captured["action"] = action
captured.update(params)
return {
"code": 202,
"success": False,
"verified": True,
"channel_used": "frida_rpc",
"trace_id": "trace-confirm",
"raw_rpc_receipt": {"started": True},
"prepared": True,
"payment_confirm_required": True,
"status": "payment_confirm_required",
"error_code": "payment_confirm_required",
"readback": {"found": False},
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
resp = asyncio.run(unified.confirm_prepared_transfer(
unified.TransferConfirmRequest(
device_id="device-confirm",
req_key="sns_tf_test_req_key",
to_id="test-user",
transfer_id="tid",
transaction_id="txid",
pay_password="123456",
confirm=True,
dry_run=False,
)
))
assert captured["action"] == "confirm_prepared_transfer"
assert captured["payment_password"] == "123456"
assert captured["password_present"] is True
assert resp["code"] == 202
assert "123456" not in str(resp)
def test_wp_hl_06_transfer_confirm_hook_mapping_and_rpc_export():
from pathlib import Path
from agent.hook import hook_executor
assert hook_executor.ACTION_TO_RPC["confirm_prepared_transfer"] == "confirmPreparedTransfer"
assert hook_executor.ACTION_ALIASES["transfer_confirm"] == "confirm_prepared_transfer"
hook_path = Path(__file__).resolve().parents[1] / "agent" / "hook" / "wechat_hook_v2.js"
source = hook_path.read_text(encoding="utf-8")
assert "function _confirmPreparedTransfer" in source
assert "confirmPreparedTransfer: function" in source
assert "wechat_internal_transfer_kinda_confirm" in source
assert "confirm_prepared_transfer_kinda_autofill" in source
def test_wp_hl_06_transfer_readback_endpoint_is_read_only(monkeypatch):
import asyncio
from routers import unified
monkeypatch.setattr(unified, "_check_device_online", lambda *args, **kwargs: "hook")
captured = {}
async def fake_execute(device_id, platform, action, params, **kwargs):
captured.update({"action": action, "params": params, "kwargs": kwargs})
return {
"success": False,
"found": False,
"verified": True,
"stage": "outgoing_transfer_message_missing",
"trace_id": "trace-readback",
"raw_rpc_receipt": {"found": False},
"_channel_used": "server/frida",
}
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
resp = asyncio.run(unified.readback_transfer(
unified.TransferReadbackRequest(
device_id="device-readback",
to_id="test-user",
req_key="req-1",
transfer_id="transfer-1",
transaction_id="txn-1",
)
))
assert resp["code"] == 202
assert resp["success"] is False
assert captured["action"] == "inspect_transfer_readback"
assert captured["kwargs"]["hook_only"] is True
assert "payment_password" not in captured["params"]