2026-06-18 13:35:17 +08:00
|
|
|
import json
|
|
|
|
|
|
2026-06-18 14:47:44 +08:00
|
|
|
import httpx
|
2026-06-18 13:35:17 +08:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from nanobot.channels import feishu as feishu_module
|
|
|
|
|
from nanobot.channels.feishu import FeishuChannel
|
|
|
|
|
from nanobot.config import loader
|
|
|
|
|
from nanobot.config.schema import Config
|
2026-07-13 13:11:46 +08:00
|
|
|
from nanobot.pairing import store as pairing_store
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _default_feishu_instance(data: dict) -> dict:
|
|
|
|
|
return data["channels"]["feishu"]["instances"][0]
|
2026-06-18 13:35:17 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feishu_login_writes_credentials_to_active_config(monkeypatch, tmp_path):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {"enabled": False, "domain": "feishu"}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
feishu_module,
|
|
|
|
|
"qr_register",
|
|
|
|
|
lambda initial_domain="feishu": {
|
|
|
|
|
"app_id": "cli_app",
|
|
|
|
|
"app_secret": "secret",
|
|
|
|
|
"domain": "lark",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-07-13 13:11:46 +08:00
|
|
|
monkeypatch.setattr(
|
|
|
|
|
feishu_module,
|
|
|
|
|
"fetch_feishu_app_identity",
|
|
|
|
|
lambda app_id, app_secret, domain: {
|
|
|
|
|
"displayName": "Voraflare Bot",
|
|
|
|
|
"avatarUrl": "https://example.com/avatar.png",
|
|
|
|
|
"identityFetchedAt": "2026-07-06T00:00:00Z",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-06-18 13:35:17 +08:00
|
|
|
|
|
|
|
|
channel = FeishuChannel({"enabled": False, "domain": "feishu"}, None)
|
|
|
|
|
|
|
|
|
|
assert await channel.login() is True
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
2026-07-13 13:11:46 +08:00
|
|
|
instance = _default_feishu_instance(data)
|
|
|
|
|
assert instance["id"] == "default"
|
|
|
|
|
assert instance["appId"] == "cli_app"
|
|
|
|
|
assert instance["appSecret"] == "secret"
|
|
|
|
|
assert instance["domain"] == "lark"
|
|
|
|
|
assert instance["identityKey"] == "lark:cli_app"
|
|
|
|
|
assert instance["enabled"] is True
|
|
|
|
|
assert instance["displayName"] == "Voraflare Bot"
|
|
|
|
|
assert instance["avatarUrl"] == "https://example.com/avatar.png"
|
|
|
|
|
assert instance["identityFetchedAt"] == "2026-07-06T00:00:00Z"
|
2026-06-18 13:35:17 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_begin_registration_requires_login_url(monkeypatch):
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
feishu_module,
|
|
|
|
|
"_post_registration",
|
|
|
|
|
lambda _base_url, _body: {"device_code": "device"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with pytest.raises(RuntimeError, match="login URL"):
|
|
|
|
|
feishu_module._begin_registration()
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 16:27:46 +08:00
|
|
|
def test_begin_registration_preserves_login_url(monkeypatch):
|
|
|
|
|
login_url = "https://accounts.feishu.cn/login?device_code=device"
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
feishu_module,
|
|
|
|
|
"_post_registration",
|
|
|
|
|
lambda _base_url, _body: {
|
|
|
|
|
"device_code": "device",
|
|
|
|
|
"verification_uri_complete": login_url,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert feishu_module._begin_registration()["qr_url"] == login_url
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 14:47:44 +08:00
|
|
|
def test_qr_register_returns_none_on_network_error(monkeypatch):
|
|
|
|
|
def raise_connect_error(_base_url, _body):
|
|
|
|
|
raise httpx.ConnectError("network down")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(feishu_module, "_post_registration", raise_connect_error)
|
|
|
|
|
|
|
|
|
|
assert feishu_module.qr_register() is None
|
|
|
|
|
|
|
|
|
|
|
2026-07-13 13:11:46 +08:00
|
|
|
def test_save_registration_result_keeps_credentials_when_identity_fetch_fails(monkeypatch, tmp_path):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
loader.save_config(Config(), config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
|
|
|
|
|
def fail_identity(_app_id, _app_secret, _domain):
|
|
|
|
|
raise RuntimeError("metadata unavailable")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(feishu_module, "fetch_feishu_app_identity", fail_identity)
|
|
|
|
|
|
|
|
|
|
feishu_module.save_registration_result({
|
|
|
|
|
"app_id": "cli_app",
|
|
|
|
|
"app_secret": "secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = _default_feishu_instance(data)
|
|
|
|
|
assert instance["appId"] == "cli_app"
|
|
|
|
|
assert instance["appSecret"] == "secret"
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_app"
|
|
|
|
|
assert "displayName" not in instance
|
|
|
|
|
assert "avatarUrl" not in instance
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_save_registration_result_resets_access_when_instance_app_changes(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
tmp_path,
|
|
|
|
|
):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
pairing_path = tmp_path / "pairing.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"instances": [
|
|
|
|
|
{
|
|
|
|
|
"id": "assistant-test",
|
|
|
|
|
"instanceId": "assistant-test",
|
|
|
|
|
"name": "old assistant",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_old",
|
|
|
|
|
"appSecret": "old-secret",
|
|
|
|
|
"identityKey": "feishu:cli_old",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
"allow_from": ["old-snake-open-id"],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(pairing_store, "_store_path", lambda: pairing_path)
|
|
|
|
|
monkeypatch.setattr(feishu_module, "fetch_feishu_app_identity", lambda *_args: {})
|
|
|
|
|
|
|
|
|
|
approved_code = pairing_store.generate_code("feishu.assistant-test", "paired-user")
|
|
|
|
|
pairing_store.approve_code(approved_code)
|
|
|
|
|
pending_code = pairing_store.generate_code("feishu.assistant-test", "pending-user")
|
|
|
|
|
|
|
|
|
|
feishu_module.save_registration_result(
|
|
|
|
|
{
|
|
|
|
|
"app_id": "cli_new",
|
|
|
|
|
"app_secret": "new-secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
},
|
|
|
|
|
instance_id="assistant-test",
|
|
|
|
|
name="new assistant",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = data["channels"]["feishu"]["instances"][0]
|
|
|
|
|
assert instance["appId"] == "cli_new"
|
|
|
|
|
assert instance["appSecret"] == "new-secret"
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_new"
|
|
|
|
|
assert instance["allowFrom"] == []
|
|
|
|
|
assert instance["allow_from"] == []
|
|
|
|
|
assert pairing_store.is_approved("feishu.assistant-test", "paired-user") is False
|
|
|
|
|
assert pairing_store.approve_code(pending_code) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_save_registration_result_keeps_access_when_only_secret_rotates(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
tmp_path,
|
|
|
|
|
):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
pairing_path = tmp_path / "pairing.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"instances": [
|
|
|
|
|
{
|
|
|
|
|
"id": "assistant-test",
|
|
|
|
|
"instanceId": "assistant-test",
|
|
|
|
|
"name": "same assistant",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_same",
|
|
|
|
|
"appSecret": "old-secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
"identityKey": "feishu:cli_same",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(pairing_store, "_store_path", lambda: pairing_path)
|
|
|
|
|
monkeypatch.setattr(feishu_module, "fetch_feishu_app_identity", lambda *_args: {})
|
|
|
|
|
|
|
|
|
|
approved_code = pairing_store.generate_code("feishu.assistant-test", "paired-user")
|
|
|
|
|
pairing_store.approve_code(approved_code)
|
|
|
|
|
pending_code = pairing_store.generate_code("feishu.assistant-test", "pending-user")
|
|
|
|
|
|
|
|
|
|
feishu_module.save_registration_result(
|
|
|
|
|
{
|
|
|
|
|
"app_id": "cli_same",
|
|
|
|
|
"app_secret": "new-secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
},
|
|
|
|
|
instance_id="assistant-test",
|
|
|
|
|
name="same assistant",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = data["channels"]["feishu"]["instances"][0]
|
|
|
|
|
assert instance["appSecret"] == "new-secret"
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_same"
|
|
|
|
|
assert instance["allowFrom"] == ["old-open-id"]
|
|
|
|
|
assert pairing_store.is_approved("feishu.assistant-test", "paired-user") is True
|
|
|
|
|
assert pairing_store.approve_code(pending_code) == (
|
|
|
|
|
"feishu.assistant-test",
|
|
|
|
|
"pending-user",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_save_registration_result_resets_access_when_domain_changes(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
tmp_path,
|
|
|
|
|
):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
pairing_path = tmp_path / "pairing.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"instances": [
|
|
|
|
|
{
|
|
|
|
|
"id": "assistant-test",
|
|
|
|
|
"instanceId": "assistant-test",
|
|
|
|
|
"name": "lark assistant",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_same",
|
|
|
|
|
"appSecret": "old-secret",
|
|
|
|
|
"domain": "lark",
|
|
|
|
|
"identityKey": "lark:cli_same",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(pairing_store, "_store_path", lambda: pairing_path)
|
|
|
|
|
monkeypatch.setattr(feishu_module, "fetch_feishu_app_identity", lambda *_args: {})
|
|
|
|
|
|
|
|
|
|
approved_code = pairing_store.generate_code("feishu.assistant-test", "paired-user")
|
|
|
|
|
pairing_store.approve_code(approved_code)
|
|
|
|
|
|
|
|
|
|
feishu_module.save_registration_result(
|
|
|
|
|
{
|
|
|
|
|
"app_id": "cli_same",
|
|
|
|
|
"app_secret": "new-secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
},
|
|
|
|
|
instance_id="assistant-test",
|
|
|
|
|
name="feishu assistant",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = data["channels"]["feishu"]["instances"][0]
|
|
|
|
|
assert instance["domain"] == "feishu"
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_same"
|
|
|
|
|
assert instance["allowFrom"] == []
|
|
|
|
|
assert pairing_store.is_approved("feishu.assistant-test", "paired-user") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sync_saved_identity_boundary_resets_access_after_manual_app_change(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
tmp_path,
|
|
|
|
|
):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
pairing_path = tmp_path / "pairing.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"instances": [
|
|
|
|
|
{
|
|
|
|
|
"id": "assistant-test",
|
|
|
|
|
"instanceId": "assistant-test",
|
|
|
|
|
"name": "manual assistant",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_new",
|
|
|
|
|
"appSecret": "secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
"identityKey": "feishu:cli_old",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
"allow_from": ["old-snake-open-id"],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(pairing_store, "_store_path", lambda: pairing_path)
|
|
|
|
|
|
|
|
|
|
approved_code = pairing_store.generate_code("feishu.assistant-test", "paired-user")
|
|
|
|
|
pairing_store.approve_code(approved_code)
|
|
|
|
|
pending_code = pairing_store.generate_code("feishu.assistant-test", "pending-user")
|
|
|
|
|
|
|
|
|
|
assert feishu_module.sync_saved_feishu_identity_boundary(
|
|
|
|
|
instance_id="assistant-test",
|
|
|
|
|
app_id="cli_new",
|
|
|
|
|
domain="feishu",
|
|
|
|
|
) is True
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = data["channels"]["feishu"]["instances"][0]
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_new"
|
|
|
|
|
assert instance["allowFrom"] == []
|
|
|
|
|
assert instance["allow_from"] == []
|
|
|
|
|
assert pairing_store.is_approved("feishu.assistant-test", "paired-user") is False
|
|
|
|
|
assert pairing_store.approve_code(pending_code) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sync_saved_identity_boundary_backfills_marker_without_resetting_access(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
tmp_path,
|
|
|
|
|
):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
pairing_path = tmp_path / "pairing.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"instances": [
|
|
|
|
|
{
|
|
|
|
|
"id": "assistant-test",
|
|
|
|
|
"instanceId": "assistant-test",
|
|
|
|
|
"name": "existing assistant",
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_existing",
|
|
|
|
|
"appSecret": "secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
monkeypatch.setattr(pairing_store, "_store_path", lambda: pairing_path)
|
|
|
|
|
|
|
|
|
|
approved_code = pairing_store.generate_code("feishu.assistant-test", "paired-user")
|
|
|
|
|
pairing_store.approve_code(approved_code)
|
|
|
|
|
|
|
|
|
|
assert feishu_module.sync_saved_feishu_identity_boundary(
|
|
|
|
|
instance_id="assistant-test",
|
|
|
|
|
app_id="cli_existing",
|
|
|
|
|
domain="feishu",
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
|
|
|
instance = data["channels"]["feishu"]["instances"][0]
|
|
|
|
|
assert instance["identityKey"] == "feishu:cli_existing"
|
|
|
|
|
assert instance["allowFrom"] == ["old-open-id"]
|
|
|
|
|
assert pairing_store.is_approved("feishu.assistant-test", "paired-user") is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_sync_saved_identity_boundary_preserves_legacy_flat_config(monkeypatch, tmp_path):
|
|
|
|
|
config_path = tmp_path / "config.json"
|
|
|
|
|
config = Config()
|
|
|
|
|
config.channels.feishu = {
|
|
|
|
|
"enabled": True,
|
|
|
|
|
"appId": "cli_existing",
|
|
|
|
|
"appSecret": "secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
"allowFrom": ["old-open-id"],
|
|
|
|
|
}
|
|
|
|
|
loader.save_config(config, config_path)
|
|
|
|
|
monkeypatch.setattr(loader, "_current_config_path", config_path)
|
|
|
|
|
|
|
|
|
|
assert feishu_module.sync_saved_feishu_identity_boundary(
|
|
|
|
|
instance_id="default",
|
|
|
|
|
app_id="cli_existing",
|
|
|
|
|
domain="feishu",
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
saved = json.loads(config_path.read_text(encoding="utf-8"))["channels"]["feishu"]
|
|
|
|
|
assert saved["appId"] == "cli_existing"
|
|
|
|
|
assert saved["appSecret"] == "secret"
|
|
|
|
|
assert saved["identityKey"] == "feishu:cli_existing"
|
|
|
|
|
assert saved["allowFrom"] == ["old-open-id"]
|
|
|
|
|
assert "instances" not in saved
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 14:00:22 +08:00
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_feishu_login_creates_missing_active_config(monkeypatch, tmp_path):
|
2026-06-18 13:35:17 +08:00
|
|
|
missing_config = tmp_path / "missing.json"
|
2026-06-18 14:00:22 +08:00
|
|
|
monkeypatch.setattr(loader, "_current_config_path", missing_config)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
feishu_module,
|
|
|
|
|
"qr_register",
|
|
|
|
|
lambda initial_domain="feishu": {
|
|
|
|
|
"app_id": "cli_app",
|
|
|
|
|
"app_secret": "secret",
|
|
|
|
|
"domain": "feishu",
|
|
|
|
|
},
|
|
|
|
|
)
|
2026-06-18 13:35:17 +08:00
|
|
|
|
2026-06-18 14:00:22 +08:00
|
|
|
channel = FeishuChannel({}, None)
|
2026-06-18 13:35:17 +08:00
|
|
|
|
2026-06-18 14:00:22 +08:00
|
|
|
assert await channel.login() is True
|
|
|
|
|
assert missing_config.exists()
|
|
|
|
|
data = json.loads(missing_config.read_text(encoding="utf-8"))
|
2026-07-13 13:11:46 +08:00
|
|
|
instance = _default_feishu_instance(data)
|
|
|
|
|
assert instance["id"] == "default"
|
|
|
|
|
assert instance["appId"] == "cli_app"
|