325 lines
11 KiB
Python
325 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fastapi import Response
|
|
|
|
SDK = Path(__file__).resolve().parents[1]
|
|
APP = SDK / "app"
|
|
if str(SDK) not in sys.path:
|
|
sys.path.insert(0, str(SDK))
|
|
if str(APP) not in sys.path:
|
|
sys.path.insert(1, str(APP))
|
|
|
|
from routers import unified
|
|
from services.device_transport import WECHAT_STRICT_FRIDA_ACTIONS
|
|
|
|
|
|
def test_hl04_params_and_action_enum_are_explicit():
|
|
req = unified.WP_HL04ActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="wechat_search",
|
|
params={"keyword": "hello", "limit": 10},
|
|
)
|
|
assert req.params.keyword == "hello"
|
|
assert len(unified.WP_HL04_ACTIONS) == 38
|
|
assert req.dry_run is True
|
|
|
|
|
|
def test_hl04_write_defaults_to_dry_run_without_rpc(monkeypatch):
|
|
called = False
|
|
|
|
async def fake_execute(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"success": True}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.WP_HL04ActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="like_video",
|
|
params={"index": 1},
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_wp_hl04(req, response))
|
|
assert called is False
|
|
assert response.status_code == 200
|
|
assert body["data"]["error_code"] == "dry_run_no_write"
|
|
assert body["data"]["raw_rpc_receipt"] is None
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_hl04_failure_sets_http_503_and_preserves_trace_receipt_readback(monkeypatch):
|
|
async def fake_execute(*args, **kwargs):
|
|
return {
|
|
"code": 200,
|
|
"success": False,
|
|
"trace_id": "trace-hl04-001",
|
|
"channel": "frida_rpc",
|
|
"error_code": "capability_unavailable",
|
|
"error_message": "rpc unavailable",
|
|
"raw_rpc_receipt": {"implemented": False},
|
|
"readback": None,
|
|
}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.WP_HL04ActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="like_video",
|
|
params={"index": 1},
|
|
trace_id="trace-hl04-001",
|
|
dry_run=False,
|
|
confirm=True,
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_wp_hl04(req, response))
|
|
assert response.status_code == 503
|
|
assert body["data"]["trace_id"] == "trace-hl04-001"
|
|
assert body["data"]["raw_rpc_receipt"] == {"implemented": False}
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_openapi_declares_headless_and_video_503():
|
|
from main import app
|
|
|
|
schema = app.openapi()
|
|
headless = schema["paths"]["/api/v3/wechat/headless/execute"]["post"]
|
|
assert {"200", "422", "503"} <= set(headless["responses"])
|
|
action = schema["components"]["schemas"]["WP_HL04ActionRequest"]["properties"]["action"]
|
|
assert len(action["enum"]) == 38
|
|
for name in ("like", "comment", "follow", "share"):
|
|
path = f"/api/v3/video-channel/{name}"
|
|
assert "503" in schema["paths"][path]["post"]["responses"]
|
|
|
|
|
|
def test_friend_group_request_has_ten_explicit_actions_and_default_dry_run():
|
|
req = unified.FriendGroupActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="set_group_notice",
|
|
params={"group_id": "GROUP_ID", "notice": "notice"},
|
|
)
|
|
assert len(unified.FRIEND_GROUP_FIRST_ACTIONS) == 10
|
|
assert req.platform == "wechat"
|
|
assert req.dry_run is True
|
|
|
|
|
|
def test_friend_group_invalid_params_is_http_422_without_rpc(monkeypatch):
|
|
called = False
|
|
|
|
async def fake_execute(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"success": True}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.FriendGroupActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="invite_to_group",
|
|
params={"group_id": "GROUP_ID"},
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_friend_group_action(req, response))
|
|
assert called is False
|
|
assert response.status_code == 422
|
|
assert body["data"]["error_code"] == "invalid_params"
|
|
assert body["data"]["raw_rpc_receipt"] is None
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_friend_group_capability_failure_is_http_503_and_preserves_receipts(monkeypatch):
|
|
async def fake_execute(*args, **kwargs):
|
|
return {
|
|
"success": False,
|
|
"trace_id": "trace-fg-001",
|
|
"channel": "frida_rpc",
|
|
"error_code": "capability_unavailable",
|
|
"error_message": "friend group rpc unavailable",
|
|
"raw_rpc_receipt": {"implemented": False},
|
|
"readback": None,
|
|
}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.FriendGroupActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="set_friend_remark",
|
|
params={"user_id": "wxid_target", "remark": "remark"},
|
|
trace_id="trace-fg-001",
|
|
dry_run=False,
|
|
confirm=True,
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_friend_group_action(req, response))
|
|
assert response.status_code == 503
|
|
assert body["data"]["trace_id"] == "trace-fg-001"
|
|
assert body["data"]["raw_rpc_receipt"] == {"implemented": False}
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_friend_group_openapi_has_422_503_and_ten_action_enum():
|
|
from main import app
|
|
|
|
schema = app.openapi()
|
|
operation = schema["paths"]["/api/v3/wechat/friend-group/execute"]["post"]
|
|
assert {"200", "422", "503"} <= set(operation["responses"])
|
|
action = schema["components"]["schemas"]["FriendGroupActionRequest"]["properties"]["action"]
|
|
assert len(action["enum"]) == 10
|
|
|
|
|
|
def test_group_welcome_requires_chatroom_and_returns_http_422_without_rpc(monkeypatch):
|
|
called = False
|
|
|
|
async def fake_execute(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"success": True}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.SetGroupWelcomeRequest(
|
|
device_id="DEVICE_ID",
|
|
group_id="invalid_group",
|
|
welcome_text="welcome",
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.set_group_welcome(req, response))
|
|
assert called is False
|
|
assert response.status_code == 422
|
|
assert body["data"]["error_code"] == "invalid_params"
|
|
assert body["data"]["raw_rpc_receipt"] is None
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_group_welcome_capability_unavailable_preserves_probe_and_readback(monkeypatch):
|
|
seen = {}
|
|
|
|
async def fake_execute(device_id, platform, action, params, **kwargs):
|
|
seen.update(action=action, params=params, kwargs=kwargs)
|
|
return {
|
|
"success": False,
|
|
"trace_id": "trace-group-welcome-001",
|
|
"channel": "frida_rpc",
|
|
"error_code": "capability_unavailable",
|
|
"error_message": "scene unavailable",
|
|
"raw_rpc_receipt": {"operation": "set_group_welcome", "probe_only": True},
|
|
"readback": {"group_id": "GROUP@chatroom", "group_found": True, "chatroom_notice": ""},
|
|
}
|
|
|
|
monkeypatch.setattr(unified, "_execute_skill", fake_execute)
|
|
req = unified.SetGroupWelcomeRequest(
|
|
device_id="DEVICE_ID",
|
|
group_id="GROUP@chatroom",
|
|
welcome_text="welcome",
|
|
dry_run=True,
|
|
confirm=True,
|
|
trace_id="trace-group-welcome-001",
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.set_group_welcome(req, response))
|
|
assert response.status_code == 503
|
|
assert seen["action"] == "set_group_welcome"
|
|
assert seen["kwargs"]["hook_only"] is True
|
|
assert seen["params"]["welcome"] == "welcome"
|
|
assert body["data"]["raw_rpc_receipt"]["probe_only"] is True
|
|
assert body["data"]["readback"]["group_found"] is True
|
|
|
|
|
|
def test_group_welcome_openapi_keeps_422_and_503_gray_contract():
|
|
from main import app
|
|
|
|
operation = app.openapi()["paths"]["/api/v3/group/set-welcome"]["post"]
|
|
assert {"200", "422", "503"} <= set(operation["responses"])
|
|
schema = app.openapi()["components"]["schemas"]["SetGroupWelcomeRequest"]
|
|
assert {"dry_run", "confirm", "trace_id"} <= set(schema["properties"])
|
|
|
|
|
|
def test_wireless_qr_request_has_five_actions_and_media_payload():
|
|
req = unified.WirelessQrActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="extract_qr_from_image",
|
|
media={"media_id": "MEDIA_ID", "mime_type": "image/png"},
|
|
)
|
|
assert len(unified.WIRELESS_QR_ACTIONS) == 5
|
|
assert set(unified.WIRELESS_QR_ACTIONS) <= WECHAT_STRICT_FRIDA_ACTIONS
|
|
assert req.media.media_id == "MEDIA_ID"
|
|
assert req.dry_run is True
|
|
|
|
|
|
def test_wireless_qr_missing_media_is_http_422_without_rpc(monkeypatch):
|
|
called = False
|
|
|
|
async def fake_execute(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"success": True}
|
|
|
|
monkeypatch.setattr(unified, "_execute_wechat_wss_frida", fake_execute)
|
|
req = unified.WirelessQrActionRequest(device_id="DEVICE_ID", action="scan_qr_code")
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_wireless_qr_action(req, response))
|
|
assert called is False
|
|
assert response.status_code == 422
|
|
assert body["data"]["error_code"] == "invalid_params"
|
|
assert body["data"]["raw_rpc_receipt"] is None
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_wireless_qr_media_decoder_unavailable_is_http_503_and_transparent(monkeypatch):
|
|
async def fake_execute(*args, **kwargs):
|
|
return {
|
|
"success": False,
|
|
"trace_id": "trace-qr-001",
|
|
"channel": "frida_rpc",
|
|
"error_code": "wireless_qr_media_decoder_unavailable",
|
|
"error_message": "wireless media decoder unavailable",
|
|
"raw_rpc_receipt": {"implemented": False},
|
|
"readback": None,
|
|
}
|
|
|
|
monkeypatch.setattr(unified, "_execute_wechat_wss_frida", fake_execute)
|
|
req = unified.WirelessQrActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="extract_qr_from_image",
|
|
media={"media_id": "MEDIA_ID"},
|
|
trace_id="trace-qr-001",
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_wireless_qr_action(req, response))
|
|
assert response.status_code == 503
|
|
assert body["data"]["error_code"] == "wireless_qr_media_decoder_unavailable"
|
|
assert body["data"]["trace_id"] == "trace-qr-001"
|
|
assert body["data"]["raw_rpc_receipt"] == {"implemented": False}
|
|
assert body["data"]["readback"] is None
|
|
|
|
|
|
def test_wireless_qr_add_friend_dry_run_does_not_execute(monkeypatch):
|
|
called = False
|
|
|
|
async def fake_execute(*args, **kwargs):
|
|
nonlocal called
|
|
called = True
|
|
return {"success": True}
|
|
|
|
monkeypatch.setattr(unified, "_execute_wechat_wss_frida", fake_execute)
|
|
req = unified.WirelessQrActionRequest(
|
|
device_id="DEVICE_ID",
|
|
action="add_friend_by_qr",
|
|
wechat_id="wxid_target",
|
|
)
|
|
response = Response()
|
|
body = asyncio.run(unified.execute_wireless_qr_action(req, response))
|
|
assert called is False
|
|
assert response.status_code == 200
|
|
assert body["data"]["error_code"] == "dry_run_no_write"
|
|
|
|
|
|
def test_wireless_qr_openapi_has_media_model_and_http_semantics():
|
|
from main import app
|
|
|
|
schema = app.openapi()
|
|
operation = schema["paths"]["/api/v3/wechat/qr/execute"]["post"]
|
|
assert {"200", "422", "503"} <= set(operation["responses"])
|
|
action = schema["components"]["schemas"]["WirelessQrActionRequest"]["properties"]["action"]
|
|
assert len(action["enum"]) == 5
|
|
assert "WirelessQrMediaPayload" in schema["components"]["schemas"]
|