feat: implement channel runtime connectors

This commit is contained in:
2026-06-03 16:22:44 +08:00
parent ee972441f5
commit c3d84b904a
105 changed files with 15621 additions and 322 deletions

View File

@ -0,0 +1,322 @@
from __future__ import annotations
import json
from urllib.parse import parse_qs
from external_connector.app import create_app
from external_connector.providers.composite import CompositeProvider
from external_connector.providers.fake import FakeProvider
from external_connector.providers.feishu_bot import FeishuBotProvider
from external_connector.providers.weixin_ilink import WeixinIlinkProvider
from external_connector.state import SidecarStateStore
from fastapi.testclient import TestClient
class FakeResponse:
def __init__(self, payload: dict[str, object]) -> None:
self.payload = payload
self.status_code = 200
self.is_success = True
self.text = json.dumps(payload)
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, object]:
return self.payload
class FakeHttpClient:
def __init__(self) -> None:
self.posts: list[tuple[str, dict[str, object] | None, dict[str, str] | None]] = []
self.registration_poll_response: dict[str, object] = {"error": "authorization_pending"}
def post(
self,
url: str,
*,
json: dict[str, object] | None = None,
data: str | None = None,
headers: dict[str, str] | None = None,
timeout: float | None = None,
) -> FakeResponse:
self.posts.append((url, json, headers))
if url.endswith("/oauth/v1/app/registration"):
params = parse_qs(data or "")
action = str((params.get("action") or [""])[0])
if action == "init":
return FakeResponse({"supported_auth_methods": ["client_secret"]})
if action == "begin":
return FakeResponse(
{
"verification_uri_complete": "https://accounts.feishu.cn/scan?device=1",
"device_code": "device-1",
"interval": 1,
"expire_in": 600,
}
)
if action == "poll":
return FakeResponse(self.registration_poll_response)
if url.endswith("/open-apis/auth/v3/tenant_access_token/internal"):
return FakeResponse({"code": 0, "tenant_access_token": "tenant-token", "expire": 7200})
if "/open-apis/im/v1/messages" in url:
return FakeResponse({"code": 0, "data": {"message_id": "om_out"}})
raise AssertionError(url)
def _provider(
tmp_path,
*,
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] | None = None,
http_client: FakeHttpClient | None = None,
receiver_starts: list[str] | None = None,
) -> FeishuBotProvider:
def bridge_post(url: str, payload: dict[str, object], headers: dict[str, str]) -> None:
if bridge_posts is not None:
bridge_posts.append((url, payload, headers))
def start_receiver(session) -> object:
if receiver_starts is not None:
receiver_starts.append(session.connection_id)
return object()
return FeishuBotProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http_client or FakeHttpClient(),
bridge_base_url="http://beaver:8080",
public_base_url="http://public-sidecar:8787",
bridge_token="bridge-token",
bridge_post=bridge_post,
receiver_start=start_receiver,
)
def test_feishu_bot_provider_starts_create_session_with_qr_from_registration(tmp_path) -> None:
provider = _provider(tmp_path)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"mode": "create", "domain": "feishu"},
}
)
assert session["status"] == "qr_ready"
assert session["qrCode"] == "https://accounts.feishu.cn/scan?device=1&from=onboard"
assert session["qrImage"].startswith("data:image/svg+xml;base64,")
assert any("一键创建飞书机器人" in item for item in session["instructions"])
assert any("/feishu start" in item for item in session["instructions"])
assert session["metadata"]["eventCallbackPath"] == "/feishu/events"
assert session["metadata"]["eventCallbackUrl"] == "http://public-sidecar:8787/feishu/events"
assert session["metadata"]["deviceCode"] == "device-1"
def test_feishu_bot_provider_poll_connects_after_qr_confirmation(tmp_path) -> None:
http = FakeHttpClient()
receiver_starts: list[str] = []
provider = _provider(tmp_path, http_client=http, receiver_starts=receiver_starts)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"mode": "create", "domain": "feishu"},
}
)
http.registration_poll_response = {
"client_id": "cli_qr",
"client_secret": "qr-secret",
"user_info": {"tenant_brand": "feishu", "open_id": "ou_me"},
}
connected = provider.get_session(session["sessionId"])
repeated = provider.get_session(session["sessionId"])
assert connected["status"] == "connected"
assert repeated["status"] == "connected"
assert connected["accountId"] == "feishu:cli_qr"
assert receiver_starts == ["conn_1"]
stored = provider.store.get_session(session["sessionId"])
assert stored.metadata["appId"] == "cli_qr"
assert stored.metadata["appSecret"] == "qr-secret"
assert stored.metadata["tenantAccessToken"] == "tenant-token"
def test_feishu_bot_provider_connects_with_app_credentials(tmp_path) -> None:
receiver_starts: list[str] = []
provider = _provider(tmp_path, receiver_starts=receiver_starts)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"appId": "cli_xxx", "appSecret": "secret", "verificationToken": "verify-token"},
}
)
assert session["status"] == "connected"
assert session["accountId"] == "feishu:cli_xxx"
assert session["displayName"] == "Feishu Main"
assert receiver_starts == ["conn_1"]
def test_feishu_bot_provider_send_uses_tenant_token_and_dedupes(tmp_path) -> None:
provider = _provider(tmp_path)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"appId": "cli_xxx", "appSecret": "secret"},
}
)
payload = {
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "feishu-main",
"kind": "feishu",
"target": {"peerId": "ou_user", "peerType": "dm", "threadId": None},
"content": "hello",
"metadata": {},
}
first = provider.send(payload)
second = provider.send(payload)
send_posts = [item for item in provider.http.posts if "/open-apis/im/v1/messages" in item[0]]
assert session["status"] == "connected"
assert first == second
assert first["providerMessageId"] == "om_out"
assert len(send_posts) == 1
assert send_posts[0][0].startswith("https://open.feishu.cn/open-apis/im/v1/messages")
assert send_posts[0][2]["Authorization"] == "Bearer tenant-token"
assert send_posts[0][1]["receive_id"] == "ou_user"
assert send_posts[0][1]["msg_type"] == "text"
def test_feishu_event_route_returns_challenge(tmp_path) -> None:
provider = _provider(tmp_path)
app = create_app(provider=provider, api_token="sidecar-token")
with TestClient(app) as client:
response = client.post("/feishu/events", json={"challenge": "abc"})
assert response.status_code == 200
assert response.json() == {"challenge": "abc"}
def test_feishu_event_route_forwards_message_to_bridge(tmp_path) -> None:
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
provider = _provider(tmp_path, bridge_posts=bridge_posts)
provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"appId": "cli_xxx", "appSecret": "secret", "verificationToken": "verify-token"},
}
)
app = create_app(provider=provider, api_token="sidecar-token")
with TestClient(app) as client:
response = client.post(
"/feishu/events",
json={
"schema": "2.0",
"header": {"event_id": "evt_1", "token": "verify-token", "app_id": "cli_xxx"},
"event": {
"sender": {"sender_id": {"open_id": "ou_user"}},
"message": {
"message_id": "om_1",
"chat_id": "oc_chat",
"chat_type": "p2p",
"message_type": "text",
"content": "{\"text\":\"hello feishu\"}",
},
},
},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
assert bridge_posts[0][0] == "http://beaver:8080/api/channel-connector-bridge/events"
assert bridge_posts[0][2]["Authorization"] == "Bearer bridge-token"
assert bridge_posts[0][1]["eventId"] == "evt_1"
assert bridge_posts[0][1]["content"] == "hello feishu"
assert bridge_posts[0][1]["peerId"] == "ou_user"
def test_composite_provider_routes_feishu_and_weixin_descriptors(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json")
provider = CompositeProvider([FakeProvider(store), _provider(tmp_path)])
connectors = provider.connectors()
assert [item["kind"] for item in connectors] == ["weixin", "feishu", "feishu"]
assert provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)["status"] == "qr_ready"
def test_composite_provider_get_session_routes_feishu_session_to_feishu_provider(tmp_path) -> None:
http = FakeHttpClient()
store = SidecarStateStore(tmp_path / "state.json")
provider = CompositeProvider(
[
WeixinIlinkProvider(
store=store,
http_client=FakeHttpClient(),
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
),
FeishuBotProvider(
store=store,
http_client=http,
bridge_base_url="http://beaver:8080",
public_base_url="http://public-sidecar:8787",
bridge_token="bridge-token",
start_receivers=False,
),
]
)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {"mode": "create", "domain": "feishu"},
}
)
http.registration_poll_response = {
"client_id": "cli_qr",
"client_secret": "qr-secret",
"user_info": {"tenant_brand": "feishu", "open_id": "ou_me"},
}
connected = provider.get_session(session["sessionId"])
assert connected["status"] == "connected"
assert connected["accountId"] == "feishu:cli_qr"

View File

@ -0,0 +1,135 @@
from __future__ import annotations
import base64
from fastapi.testclient import TestClient
from external_connector.app import create_app
from external_connector.providers.fake import FakeProvider
from external_connector.state import SidecarStateStore
def test_fake_provider_lists_weixin_and_feishu(tmp_path) -> None:
provider = FakeProvider(SidecarStateStore(tmp_path / "state.json"))
connectors = provider.connectors()
assert [item["kind"] for item in connectors] == ["weixin", "feishu"]
assert connectors[0]["authType"] == "qr"
def test_fake_provider_session_flow(tmp_path) -> None:
provider = FakeProvider(SidecarStateStore(tmp_path / "state.json"))
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
loaded = provider.get_session(session["sessionId"])
assert session["status"] == "qr_ready"
prefix = "data:image/svg+xml;base64,"
assert session["qrImage"].startswith(prefix)
svg = base64.b64decode(session["qrImage"][len(prefix) :]).decode("utf-8")
assert svg.startswith("<svg")
assert "FAKE QR" in svg
assert loaded["sessionId"] == session["sessionId"]
def test_fake_provider_send_returns_idempotent_result(tmp_path) -> None:
provider = FakeProvider(SidecarStateStore(tmp_path / "state.json"))
payload = {
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "peer-1", "peerType": "dm", "threadId": None},
"content": "hello",
"metadata": {},
}
first = provider.send(payload)
second = provider.send(payload)
assert first == second
assert first["ok"] is True
def test_sidecar_http_api_requires_bearer_token(tmp_path) -> None:
app = create_app(provider=FakeProvider(SidecarStateStore(tmp_path / "state.json")), api_token="sidecar-token")
with TestClient(app) as client:
response = client.get("/connectors")
assert response.status_code == 401
def test_sidecar_http_api_session_and_send(tmp_path) -> None:
app = create_app(provider=FakeProvider(SidecarStateStore(tmp_path / "state.json")), api_token="sidecar-token")
headers = {"Authorization": "Bearer sidecar-token"}
with TestClient(app) as client:
connectors = client.get("/connectors", headers=headers)
session = client.post(
"/connector-sessions",
headers=headers,
json={
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
},
)
session_id = session.json()["sessionId"]
loaded = client.get(f"/connector-sessions/{session_id}", headers=headers)
sent = client.post(
"/send",
headers=headers,
json={
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "peer-1", "peerType": "dm", "threadId": None},
"content": "hello",
"metadata": {},
},
)
assert connectors.status_code == 200
assert session.status_code == 200
assert loaded.json()["sessionId"] == session_id
assert sent.json()["ok"] is True
def test_sidecar_http_api_returns_conflict_for_processing_send(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json", send_processing_ttl_seconds=60)
store.begin_send(connection_id="conn_1", request_id="out_1")
app = create_app(provider=FakeProvider(store), api_token="sidecar-token")
headers = {"Authorization": "Bearer sidecar-token"}
with TestClient(app) as client:
response = client.post(
"/send",
headers=headers,
json={
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "peer-1", "peerType": "dm", "threadId": None},
"content": "hello",
"metadata": {},
},
)
assert response.status_code == 409
assert response.json()["retryAfterSeconds"] == 5

View File

@ -0,0 +1,68 @@
from __future__ import annotations
from external_connector.state import SidecarStateStore
def test_state_store_saves_and_loads_connector_sessions(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json")
session = store.create_session(
kind="weixin",
connection_id="conn_1",
channel_id="weixin-main",
display_name="Weixin Main",
options={},
)
store.update_session(session.session_id, status="connected", account_id="weixin:me", display_name="Me")
loaded = store.get_session(session.session_id)
assert session.session_id.startswith("cs_")
assert loaded.status == "connected"
assert loaded.account_id == "weixin:me"
def test_state_store_dedupes_send_results(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json")
first = store.begin_send(connection_id="conn_1", request_id="out_1")
store.complete_send(first.dedupe_key, provider_message_id="provider-1")
duplicate = store.begin_send(connection_id="conn_1", request_id="out_1")
assert first.should_send is True
assert duplicate.should_send is False
assert duplicate.status == "completed"
assert duplicate.http_status == 200
assert duplicate.provider_message_id == "provider-1"
def test_state_store_returns_conflict_for_active_send_processing(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json", send_processing_ttl_seconds=60)
store.begin_send(connection_id="conn_1", request_id="out_1")
duplicate = store.begin_send(connection_id="conn_1", request_id="out_1")
assert duplicate.should_send is False
assert duplicate.status == "processing"
assert duplicate.http_status == 409
assert duplicate.retry_after_seconds == 5
def test_state_store_retries_stale_send_processing(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json", send_processing_ttl_seconds=0)
store.begin_send(connection_id="conn_1", request_id="out_1")
retry = store.begin_send(connection_id="conn_1", request_id="out_1")
assert retry.should_send is True
assert retry.status == "processing"
def test_state_store_retries_failed_send_immediately(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json", send_processing_ttl_seconds=60)
first = store.begin_send(connection_id="conn_1", request_id="out_1")
store.fail_send(first.dedupe_key, error="provider rejected message")
retry = store.begin_send(connection_id="conn_1", request_id="out_1")
assert retry.should_send is True
assert retry.status == "processing"

View File

@ -0,0 +1,182 @@
from __future__ import annotations
import json
from pathlib import Path
from external_connector.providers.vendor_cli import VendorCliProvider
from external_connector.state import SidecarStateStore
class FakeRunner:
def __init__(self) -> None:
self.commands: list[list[str]] = []
self.cwd: str | None = None
self.timeout: float | None = None
def __call__(self, command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
self.commands.append(command)
self.cwd = cwd
self.timeout = timeout
return 0, "connected account=weixin:me", ""
def test_vendor_cli_provider_uses_env_command_templates(tmp_path) -> None:
runner = FakeRunner()
provider = VendorCliProvider(
store=SidecarStateStore(tmp_path / "state.json"),
env={
"WEIXIN_CONNECT_COMMAND": "vendor-weixin install --state {state_dir}",
"CONNECTOR_COMMAND_TIMEOUT_SECONDS": "30",
},
runner=runner,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
assert session["status"] in {"waiting_for_user", "connected"}
assert runner.commands[0][0] == "vendor-weixin"
assert runner.cwd == str(tmp_path)
assert runner.timeout == 30.0
def test_vendor_cli_provider_redacts_sensitive_error(tmp_path) -> None:
def runner(command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
return 1, "", "failed secret-token appSecret=abc"
provider = VendorCliProvider(
store=SidecarStateStore(tmp_path / "state.json"),
env={"FEISHU_CONNECT_COMMAND": "vendor-feishu install --secret abc"},
runner=runner,
)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
assert session["status"] == "error"
assert "secret-token" not in (session["error"] or "")
assert "appSecret=abc" not in (session["error"] or "")
def test_vendor_cli_provider_refreshes_session_from_status_command_json(tmp_path) -> None:
calls: list[list[str]] = []
def runner(command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
calls.append(command)
if command[0] == "vendor-weixin-status":
return (
0,
'{"status":"qr_ready","qrImage":"data:image/png;base64,abc","qrCode":"weixin://scan","metadata":{"phase":"scan"}}',
"",
)
return 0, "waiting", ""
provider = VendorCliProvider(
store=SidecarStateStore(tmp_path / "state.json"),
env={
"WEIXIN_CONNECT_COMMAND": "vendor-weixin install --state {state_dir}",
"WEIXIN_STATUS_COMMAND": "vendor-weixin-status --state {state_dir} --session {session_id}",
},
runner=runner,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
refreshed = provider.get_session(session["sessionId"])
assert calls[1][0] == "vendor-weixin-status"
assert refreshed["status"] == "qr_ready"
assert refreshed["qrImage"] == "data:image/png;base64,abc"
assert refreshed["qrCode"] == "weixin://scan"
assert refreshed["metadata"]["phase"] == "scan"
def test_vendor_cli_provider_refreshes_connected_session_from_key_value_status(tmp_path) -> None:
def runner(command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
if command[0] == "vendor-feishu-status":
return 0, "status=connected accountId=feishu:tenant-bot displayName=FeishuBot", ""
return 0, "waiting", ""
provider = VendorCliProvider(
store=SidecarStateStore(tmp_path / "state.json"),
env={
"FEISHU_CONNECT_COMMAND": "vendor-feishu install --state {state_dir}",
"FEISHU_STATUS_COMMAND": "vendor-feishu-status --state {state_dir}",
},
runner=runner,
)
session = provider.start_session(
{
"kind": "feishu",
"connectionId": "conn_1",
"channelId": "feishu-main",
"displayName": "Feishu Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
refreshed = provider.get_session(session["sessionId"])
assert refreshed["status"] == "connected"
assert refreshed["accountId"] == "feishu:tenant-bot"
assert refreshed["displayName"] == "FeishuBot"
def test_vendor_cli_provider_send_uses_payload_file_and_dedupes_result(tmp_path) -> None:
payloads: list[dict[str, object]] = []
commands: list[list[str]] = []
def runner(command: list[str], cwd: str, timeout: float) -> tuple[int, str, str]:
commands.append(command)
payload_path = Path(command[command.index("--payload") + 1])
payloads.append(json.loads(payload_path.read_text(encoding="utf-8")))
return 0, '{"providerMessageId":"wx-msg-1"}', ""
provider = VendorCliProvider(
store=SidecarStateStore(tmp_path / "state.json"),
env={"WEIXIN_SEND_COMMAND": "vendor-weixin-send --payload {payload_path}"},
runner=runner,
)
payload = {
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "peer-1", "peerType": "dm", "threadId": None},
"content": "hello world",
"metadata": {"source": "test"},
}
first = provider.send(payload)
duplicate = provider.send(payload)
assert first == {"ok": True, "providerMessageId": "wx-msg-1"}
assert duplicate == first
assert len(commands) == 1
assert payloads[0]["content"] == "hello world"
assert payloads[0]["target"] == {"peerId": "peer-1", "peerType": "dm", "threadId": None}

View File

@ -0,0 +1,408 @@
from __future__ import annotations
import json
from external_connector.providers.weixin_ilink import WeixinIlinkProvider
from external_connector.state import SidecarStateStore
class FakeResponse:
def __init__(self, payload: dict[str, object]) -> None:
self.payload = payload
self.text = json.dumps(payload)
self.status_code = 200
self.is_success = True
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, object]:
return self.payload
class FakeHttpClient:
def __init__(self) -> None:
self.posts: list[tuple[str, dict[str, object] | None, dict[str, str] | None]] = []
self.gets: list[str] = []
def post(self, url: str, *, json: dict[str, object] | None = None, headers: dict[str, str] | None = None, timeout: float | None = None) -> FakeResponse:
self.posts.append((url, json, headers))
if "get_bot_qrcode" in url:
return FakeResponse({"qrcode": "qr-token", "qrcode_img_content": "https://scan.example/qr"})
if "sendmessage" in url:
return FakeResponse({"ret": 0})
if "getupdates" in url:
return FakeResponse(
{
"ret": 0,
"get_updates_buf": "next-buf",
"msgs": [
{
"message_id": 42,
"from_user_id": "wx-user",
"to_user_id": "wx-bot",
"context_token": "ctx-1",
"item_list": [{"type": 1, "text_item": {"text": "hello"}}],
}
],
}
)
raise AssertionError(url)
def get(self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None) -> FakeResponse:
self.gets.append(url)
return FakeResponse(
{
"status": "confirmed",
"bot_token": "bot-token",
"ilink_bot_id": "bot-1@im.bot",
"baseurl": "https://api.weixin.example",
"ilink_user_id": "wx-owner",
}
)
class BusinessFailingSendHttpClient(FakeHttpClient):
def post(self, url: str, *, json: dict[str, object] | None = None, headers: dict[str, str] | None = None, timeout: float | None = None) -> FakeResponse:
self.posts.append((url, json, headers))
if "sendmessage" in url:
return FakeResponse({"ret": 47001, "errmsg": "invalid receiver"})
return super().post(url, json=json, headers=headers, timeout=timeout)
class ScannedAfterConnectedHttpClient(FakeHttpClient):
def __init__(self) -> None:
super().__init__()
self.get_count = 0
def get(self, url: str, *, headers: dict[str, str] | None = None, timeout: float | None = None) -> FakeResponse:
self.gets.append(url)
self.get_count += 1
if self.get_count == 1:
return FakeResponse(
{
"status": "confirmed",
"bot_token": "bot-token",
"ilink_bot_id": "bot-1@im.bot",
"baseurl": "https://api.weixin.example",
"ilink_user_id": "wx-owner",
}
)
return FakeResponse({"status": "scaned"})
def test_weixin_ilink_provider_starts_real_qr_session(tmp_path) -> None:
http = FakeHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
assert session["status"] == "qr_ready"
assert session["qrCode"] == "https://scan.example/qr"
assert session["qrImage"].startswith("data:image/svg+xml;base64,")
assert http.posts[0][0].endswith("/ilink/bot/get_bot_qrcode?bot_type=3")
def test_weixin_ilink_provider_connects_on_confirmed_status(tmp_path) -> None:
http = FakeHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
connected = provider.get_session(session["sessionId"])
assert connected["status"] == "connected"
assert connected["accountId"] == "weixin:bot-1@im.bot"
assert connected["displayName"] == "Weixin Main"
def test_weixin_ilink_provider_does_not_downgrade_token_session_to_scanned(tmp_path) -> None:
http = ScannedAfterConnectedHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
connected = provider.get_session(session["sessionId"])
refreshed = provider.get_session(session["sessionId"])
assert connected["status"] == "connected"
assert refreshed["status"] == "connected"
assert refreshed["accountId"] == "weixin:bot-1@im.bot"
def test_weixin_ilink_provider_recovers_token_session_persisted_as_scanned(tmp_path) -> None:
store = SidecarStateStore(tmp_path / "state.json")
provider = WeixinIlinkProvider(
store=store,
http_client=FakeHttpClient(),
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = store.create_session(
kind="weixin",
connection_id="conn_1",
channel_id="weixin-main",
display_name="Weixin Main",
options={},
)
session = store.update_session(
session.session_id,
status="scanned",
account_id="weixin:bot-1@im.bot",
metadata={
"token": "bot-token",
"baseUrl": "https://api.weixin.example",
"userId": "wx-owner",
"getUpdatesBuf": "buf",
},
)
recovered = provider.get_session(session.session_id)
assert recovered["status"] == "connected"
assert recovered["accountId"] == "weixin:bot-1@im.bot"
def test_weixin_ilink_provider_send_uses_saved_token_and_dedupes(tmp_path) -> None:
http = FakeHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
provider.get_session(session["sessionId"])
payload = {
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "wx-user", "peerType": "dm", "threadId": None},
"content": "reply",
"metadata": {"contextToken": "ctx-1"},
}
first = provider.send(payload)
second = provider.send(payload)
send_posts = [item for item in http.posts if "sendmessage" in item[0]]
assert first == second
assert first["ok"] is True
assert len(send_posts) == 1
assert send_posts[0][2]["Authorization"] == "Bearer bot-token"
assert send_posts[0][1]["msg"]["from_user_id"] == ""
assert send_posts[0][1]["msg"]["to_user_id"] == "wx-user"
assert send_posts[0][1]["msg"]["client_id"] == "out_1"
assert send_posts[0][1]["msg"]["message_type"] == 2
assert send_posts[0][1]["msg"]["message_state"] == 2
assert send_posts[0][1]["msg"]["context_token"] == "ctx-1"
assert send_posts[0][1]["msg"]["item_list"][0]["text_item"]["text"] == "reply"
def test_weixin_ilink_provider_send_uses_cached_context_token(tmp_path) -> None:
http = FakeHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
bridge_post=lambda url, payload, headers: None,
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
provider.get_session(session["sessionId"])
provider.poll_once("conn_1")
result = provider.send(
{
"requestId": "out_2",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "wx-user", "peerType": "dm", "threadId": None},
"content": "reply",
"metadata": {},
}
)
send_posts = [item for item in http.posts if "sendmessage" in item[0]]
assert result["ok"] is True
assert send_posts[-1][1]["msg"]["context_token"] == "ctx-1"
def test_weixin_ilink_provider_send_uses_safe_client_id_for_platform(tmp_path) -> None:
http = FakeHttpClient()
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
provider.get_session(session["sessionId"])
provider.send(
{
"requestId": "out_weixin-main:account@im.bot:peer@im.wechat:msg-1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "wx-user", "peerType": "dm", "threadId": None},
"content": "reply",
"metadata": {"contextToken": "ctx-1"},
}
)
send_posts = [item for item in http.posts if "sendmessage" in item[0]]
client_id = send_posts[-1][1]["msg"]["client_id"]
assert client_id.startswith("beaver-weixin-")
assert ":" not in client_id
assert "@" not in client_id
def test_weixin_ilink_provider_send_rejects_business_error_without_completing(tmp_path) -> None:
http = BusinessFailingSendHttpClient()
store = SidecarStateStore(tmp_path / "state.json")
provider = WeixinIlinkProvider(
store=store,
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
provider.get_session(session["sessionId"])
payload = {
"requestId": "out_1",
"connectionId": "conn_1",
"channelId": "weixin-main",
"kind": "weixin",
"target": {"peerId": "wx-user", "peerType": "dm", "threadId": None},
"content": "reply",
"metadata": {},
}
result = provider.send(payload)
retry = store.begin_send(connection_id="conn_1", request_id="out_1")
assert result["ok"] is False
assert "invalid receiver" in result["error"]
assert retry.should_send is True
def test_weixin_ilink_provider_poll_once_forwards_bridge_event(tmp_path) -> None:
http = FakeHttpClient()
bridge_posts: list[tuple[str, dict[str, object], dict[str, str]]] = []
def bridge_post(url: str, payload: dict[str, object], headers: dict[str, str]) -> None:
bridge_posts.append((url, payload, headers))
provider = WeixinIlinkProvider(
store=SidecarStateStore(tmp_path / "state.json"),
http_client=http,
bridge_base_url="http://beaver:8080",
bridge_token="bridge-token",
bridge_post=bridge_post,
start_receivers=False,
)
session = provider.start_session(
{
"kind": "weixin",
"connectionId": "conn_1",
"channelId": "weixin-main",
"displayName": "Weixin Main",
"callbackBaseUrl": "http://beaver:8080",
"options": {},
}
)
provider.get_session(session["sessionId"])
provider.poll_once("conn_1")
assert bridge_posts[0][0] == "http://beaver:8080/api/channel-connector-bridge/events"
assert bridge_posts[0][2]["Authorization"] == "Bearer bridge-token"
assert bridge_posts[0][1]["eventId"] == "weixin-main:42"
assert bridge_posts[0][1]["content"] == "hello"
assert bridge_posts[0][1]["peerId"] == "wx-user"