mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 18:05:58 +00:00
853 lines
29 KiB
Python
853 lines
29 KiB
Python
"""Router tests for browser-connectable IM channels."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tempfile import TemporaryDirectory
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
from uuid import UUID
|
|
|
|
import pytest
|
|
from _router_auth_helpers import make_authed_test_app
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.channels.runtime_config_store import ChannelRuntimeConfigStore
|
|
from app.gateway.auth.models import User
|
|
from app.gateway.routers import channel_connections
|
|
from deerflow.config.app_config import AppConfig, reset_app_config, set_app_config
|
|
from deerflow.config.channel_connections_config import ChannelConnectionsConfig
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _stub_app_config():
|
|
"""Keep router tests independent from a developer-local config.yaml."""
|
|
set_app_config(AppConfig.model_validate({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}}))
|
|
yield
|
|
reset_app_config()
|
|
|
|
|
|
def _user() -> User:
|
|
return User(
|
|
id=UUID("11111111-2222-3333-4444-555555555555"),
|
|
email="alice@example.com",
|
|
password_hash="x",
|
|
system_role="user",
|
|
)
|
|
|
|
|
|
async def _make_repo(tmp_path):
|
|
from deerflow.persistence.channel_connections import ChannelConnectionRepository
|
|
from deerflow.persistence.engine import get_session_factory, init_engine
|
|
|
|
await init_engine("sqlite", url=f"sqlite+aiosqlite:///{tmp_path / 'router.db'}", sqlite_dir=str(tmp_path))
|
|
return ChannelConnectionRepository(get_session_factory())
|
|
|
|
|
|
def _make_app(
|
|
config: ChannelConnectionsConfig,
|
|
repo,
|
|
channels_config: dict | None = None,
|
|
*,
|
|
runtime_config_store: ChannelRuntimeConfigStore | None = None,
|
|
set_channels_config_state: bool = True,
|
|
):
|
|
app = make_authed_test_app(user_factory=_user)
|
|
app.state.channel_connections_config = config
|
|
app.state.channel_connection_repo = repo
|
|
if set_channels_config_state:
|
|
app.state.channels_config = channels_config or {}
|
|
if runtime_config_store is None:
|
|
runtime_config_dir = TemporaryDirectory()
|
|
app.state.channel_runtime_config_tmpdir = runtime_config_dir
|
|
runtime_config_store = ChannelRuntimeConfigStore(f"{runtime_config_dir.name}/runtime-config.json")
|
|
app.state.channel_runtime_config_store = runtime_config_store
|
|
app.include_router(channel_connections.router)
|
|
return app
|
|
|
|
|
|
def _enabled_connections_config() -> ChannelConnectionsConfig:
|
|
return ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"telegram": {"enabled": True, "bot_username": "deerflow_bot"},
|
|
"slack": {"enabled": True},
|
|
"discord": {"enabled": True},
|
|
"feishu": {"enabled": True},
|
|
"dingtalk": {"enabled": True},
|
|
"wechat": {"enabled": True},
|
|
"wecom": {"enabled": True},
|
|
}
|
|
)
|
|
|
|
|
|
def _channels_config() -> dict:
|
|
return {
|
|
"telegram": {"enabled": True, "bot_token": "telegram-token"},
|
|
"slack": {"enabled": True, "bot_token": "xoxb-operator", "app_token": "xapp-operator"},
|
|
"discord": {"enabled": True, "bot_token": "discord-bot"},
|
|
"feishu": {"enabled": True, "app_id": "feishu-app", "app_secret": "feishu-secret"},
|
|
"dingtalk": {"enabled": True, "client_id": "dingtalk-client", "client_secret": "dingtalk-secret"},
|
|
"wechat": {"enabled": True, "bot_token": "wechat-token"},
|
|
"wecom": {"enabled": True, "bot_id": "wecom-bot", "bot_secret": "wecom-secret"},
|
|
}
|
|
|
|
|
|
def test_get_providers_only_returns_enabled_channels_and_setup_fields(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"slack": {"enabled": True},
|
|
"discord": {"enabled": False},
|
|
}
|
|
)
|
|
app = _make_app(config, repo, {})
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["enabled"] is True
|
|
assert [provider["provider"] for provider in body["providers"]] == ["slack"]
|
|
assert body["providers"][0]["configured"] is False
|
|
assert body["providers"][0]["connectable"] is False
|
|
assert body["providers"][0]["credential_fields"] == [
|
|
{
|
|
"name": "bot_token",
|
|
"label": "Bot token",
|
|
"type": "password",
|
|
"required": True,
|
|
},
|
|
{
|
|
"name": "app_token",
|
|
"label": "App token",
|
|
"type": "password",
|
|
"required": True,
|
|
},
|
|
]
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_providers_uses_existing_channels_config(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["enabled"] is True
|
|
by_provider = {item["provider"]: item for item in body["providers"]}
|
|
assert set(by_provider) == {"telegram", "slack", "discord", "feishu", "dingtalk", "wechat", "wecom"}
|
|
assert by_provider["telegram"]["configured"] is True
|
|
assert by_provider["telegram"]["auth_mode"] == "deep_link"
|
|
assert by_provider["telegram"]["credential_values"] == {
|
|
"bot_token": "********",
|
|
"bot_username": "deerflow_bot",
|
|
}
|
|
assert by_provider["slack"]["configured"] is True
|
|
assert by_provider["slack"]["auth_mode"] == "binding_code"
|
|
assert by_provider["slack"]["connection_status"] == "connected"
|
|
assert by_provider["slack"]["credential_values"] == {
|
|
"bot_token": "********",
|
|
"app_token": "********",
|
|
}
|
|
assert by_provider["discord"]["configured"] is True
|
|
assert by_provider["discord"]["auth_mode"] == "binding_code"
|
|
assert by_provider["discord"]["credential_values"] == {"bot_token": "********"}
|
|
assert by_provider["feishu"]["configured"] is True
|
|
assert by_provider["feishu"]["auth_mode"] == "binding_code"
|
|
assert by_provider["feishu"]["connection_status"] == "connected"
|
|
assert by_provider["feishu"]["credential_values"] == {
|
|
"app_id": "feishu-app",
|
|
"app_secret": "********",
|
|
}
|
|
assert by_provider["dingtalk"]["configured"] is True
|
|
assert by_provider["dingtalk"]["auth_mode"] == "binding_code"
|
|
assert by_provider["dingtalk"]["credential_values"] == {
|
|
"client_id": "dingtalk-client",
|
|
"client_secret": "********",
|
|
}
|
|
assert by_provider["wechat"]["configured"] is True
|
|
assert by_provider["wechat"]["auth_mode"] == "binding_code"
|
|
assert by_provider["wechat"]["credential_values"] == {"bot_token": "********"}
|
|
assert by_provider["wecom"]["configured"] is True
|
|
assert by_provider["wecom"]["auth_mode"] == "binding_code"
|
|
assert by_provider["wecom"]["credential_values"] == {
|
|
"bot_id": "wecom-bot",
|
|
"bot_secret": "********",
|
|
}
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_providers_reports_unconfigured_when_runtime_channel_is_missing(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, {"telegram": {"enabled": True, "bot_token": "telegram-token"}})
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in response.json()["providers"]}
|
|
assert by_provider["telegram"]["configured"] is True
|
|
assert by_provider["slack"]["configured"] is False
|
|
assert by_provider["slack"]["connectable"] is False
|
|
assert "Slack credentials" in by_provider["slack"]["unavailable_reason"]
|
|
assert by_provider["discord"]["configured"] is False
|
|
assert "Discord credentials" in by_provider["discord"]["unavailable_reason"]
|
|
assert by_provider["feishu"]["configured"] is False
|
|
assert "Feishu credentials" in by_provider["feishu"]["unavailable_reason"]
|
|
assert by_provider["dingtalk"]["configured"] is False
|
|
assert "DingTalk credentials" in by_provider["dingtalk"]["unavailable_reason"]
|
|
assert by_provider["wechat"]["configured"] is False
|
|
assert "WeChat credentials" in by_provider["wechat"]["unavailable_reason"]
|
|
assert by_provider["wecom"]["configured"] is False
|
|
assert "WeCom credentials" in by_provider["wecom"]["unavailable_reason"]
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_providers_reports_configured_channel_not_running(tmp_path, monkeypatch):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
service = SimpleNamespace(
|
|
get_status=lambda: {
|
|
"service_running": True,
|
|
"channels": {
|
|
"feishu": {
|
|
"enabled": True,
|
|
"running": False,
|
|
}
|
|
},
|
|
}
|
|
)
|
|
monkeypatch.setattr("app.channels.service.get_channel_service", lambda: service)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in response.json()["providers"]}
|
|
assert by_provider["feishu"]["configured"] is True
|
|
assert by_provider["feishu"]["connectable"] is False
|
|
assert by_provider["feishu"]["connection_status"] == "not_connected"
|
|
assert "configured but is not running" in by_provider["feishu"]["unavailable_reason"]
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_providers_restarts_configured_channel_when_service_can_reconcile(tmp_path, monkeypatch):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"feishu": {"enabled": True},
|
|
}
|
|
)
|
|
channels_config = {
|
|
"feishu": {
|
|
"enabled": True,
|
|
"app_id": "feishu-app",
|
|
"app_secret": "feishu-secret",
|
|
}
|
|
}
|
|
app = _make_app(config, repo, channels_config)
|
|
status = {
|
|
"service_running": True,
|
|
"channels": {
|
|
"feishu": {
|
|
"enabled": True,
|
|
"running": False,
|
|
}
|
|
},
|
|
}
|
|
reconciled: list[tuple[str, dict]] = []
|
|
|
|
async def ensure_channel_ready(provider, runtime_config):
|
|
reconciled.append((provider, dict(runtime_config)))
|
|
status["channels"][provider]["running"] = True
|
|
return True
|
|
|
|
service = SimpleNamespace(
|
|
get_status=lambda: status,
|
|
ensure_channel_ready=ensure_channel_ready,
|
|
)
|
|
monkeypatch.setattr("app.channels.service.get_channel_service", lambda: service)
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in response.json()["providers"]}
|
|
assert by_provider["feishu"]["configured"] is True
|
|
assert by_provider["feishu"]["connectable"] is True
|
|
assert by_provider["feishu"]["connection_status"] == "connected"
|
|
assert by_provider["feishu"]["unavailable_reason"] is None
|
|
assert reconciled == [("feishu", channels_config["feishu"])]
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_providers_uses_newest_connection_status_per_provider(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
|
|
async def seed_connections():
|
|
await repo.upsert_connection(
|
|
owner_user_id=str(_user().id),
|
|
provider="slack",
|
|
external_account_id="U-old",
|
|
workspace_id="T-old",
|
|
status="revoked",
|
|
)
|
|
await anyio.sleep(0.01)
|
|
await repo.upsert_connection(
|
|
owner_user_id=str(_user().id),
|
|
provider="slack",
|
|
external_account_id="U-new",
|
|
workspace_id="T-new",
|
|
status="connected",
|
|
)
|
|
|
|
anyio.run(seed_connections)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in response.json()["providers"]}
|
|
assert by_provider["slack"]["connection_status"] == "connected"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_get_connections_returns_current_user_connections_only(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
|
|
async def seed_connections():
|
|
await repo.upsert_connection(
|
|
owner_user_id=str(_user().id),
|
|
provider="telegram",
|
|
external_account_id="42",
|
|
external_account_name="Alice",
|
|
status="connected",
|
|
)
|
|
await repo.upsert_connection(
|
|
owner_user_id="other-user",
|
|
provider="telegram",
|
|
external_account_id="99",
|
|
external_account_name="Bob",
|
|
status="connected",
|
|
)
|
|
|
|
anyio.run(seed_connections)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.get("/api/channels/connections")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert len(body["connections"]) == 1
|
|
assert body["connections"][0]["provider"] == "telegram"
|
|
assert body["connections"][0]["external_account_id"] == "42"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_connect_telegram_returns_deep_link_and_persists_state(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.post("/api/channels/telegram/connect")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["provider"] == "telegram"
|
|
assert body["mode"] == "deep_link"
|
|
assert body["url"].startswith("https://t.me/deerflow_bot?start=")
|
|
assert body["code"]
|
|
assert "/start" in body["instruction"]
|
|
|
|
async def count_states():
|
|
return await repo.count_oauth_states(owner_user_id=str(_user().id), provider="telegram")
|
|
|
|
assert anyio.run(count_states) == 1
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_connect_slack_returns_binding_command_and_persists_state(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.post("/api/channels/slack/connect")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["provider"] == "slack"
|
|
assert body["mode"] == "binding_code"
|
|
assert body["url"] is None
|
|
assert len(body["code"]) >= 22
|
|
assert body["instruction"] == f"Send /connect {body['code']} to the DeerFlow Slack bot."
|
|
|
|
async def count_states():
|
|
return await repo.count_oauth_states(owner_user_id=str(_user().id), provider="slack")
|
|
|
|
assert anyio.run(count_states) == 1
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_connect_discord_returns_binding_command_and_persists_state(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.post("/api/channels/discord/connect")
|
|
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["provider"] == "discord"
|
|
assert body["mode"] == "binding_code"
|
|
assert body["url"] is None
|
|
assert body["code"]
|
|
assert body["instruction"] == f"Send /connect {body['code']} to the DeerFlow Discord bot."
|
|
|
|
async def count_states():
|
|
return await repo.count_oauth_states(owner_user_id=str(_user().id), provider="discord")
|
|
|
|
assert anyio.run(count_states) == 1
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_connect_existing_binding_code_channels_return_command_and_persist_state(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
providers = ["feishu", "dingtalk", "wechat", "wecom"]
|
|
with TestClient(app) as client:
|
|
responses = {provider: client.post(f"/api/channels/{provider}/connect") for provider in providers}
|
|
|
|
for provider, response in responses.items():
|
|
expected_display_name = {
|
|
"feishu": "Feishu",
|
|
"dingtalk": "DingTalk",
|
|
"wechat": "WeChat",
|
|
"wecom": "WeCom",
|
|
}[provider]
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["provider"] == provider
|
|
assert body["mode"] == "binding_code"
|
|
assert body["url"] is None
|
|
assert len(body["code"]) >= 22
|
|
assert body["instruction"] == f"Send /connect {body['code']} to the DeerFlow {expected_display_name} bot."
|
|
|
|
async def count_states(provider=provider):
|
|
return await repo.count_oauth_states(owner_user_id=str(_user().id), provider=provider)
|
|
|
|
assert anyio.run(count_states) == 1
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_connect_unconfigured_runtime_channel_returns_400(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
app = _make_app(_enabled_connections_config(), repo, {})
|
|
|
|
with TestClient(app) as client:
|
|
response = client.post("/api/channels/slack/connect")
|
|
|
|
assert response.status_code == 400
|
|
assert "Slack credentials" in response.json()["detail"]
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_configure_provider_runtime_credentials_enables_connect_without_file_edits(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"slack": {"enabled": True},
|
|
}
|
|
)
|
|
app = _make_app(config, repo, {})
|
|
|
|
with TestClient(app) as client:
|
|
configure_response = client.post(
|
|
"/api/channels/slack/runtime-config",
|
|
json={"values": {"bot_token": "xoxb-ui", "app_token": "xapp-ui"}},
|
|
)
|
|
connect_response = client.post("/api/channels/slack/connect")
|
|
|
|
assert configure_response.status_code == 200
|
|
configured = configure_response.json()
|
|
assert configured["provider"] == "slack"
|
|
assert configured["configured"] is True
|
|
assert configured["connectable"] is True
|
|
assert configured["connection_status"] == "connected"
|
|
assert app.state.channels_config["slack"] == {
|
|
"enabled": True,
|
|
"bot_token": "xoxb-ui",
|
|
"app_token": "xapp-ui",
|
|
}
|
|
assert connect_response.status_code == 200
|
|
assert connect_response.json()["provider"] == "slack"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_configure_provider_runtime_credentials_survive_local_restart(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"slack": {"enabled": True},
|
|
}
|
|
)
|
|
runtime_config_path = tmp_path / "channels" / "runtime-config.json"
|
|
first_app = _make_app(
|
|
config,
|
|
repo,
|
|
{},
|
|
runtime_config_store=ChannelRuntimeConfigStore(runtime_config_path),
|
|
)
|
|
|
|
with TestClient(first_app) as client:
|
|
configure_response = client.post(
|
|
"/api/channels/slack/runtime-config",
|
|
json={"values": {"bot_token": "xoxb-ui", "app_token": "xapp-ui"}},
|
|
)
|
|
|
|
assert configure_response.status_code == 200
|
|
|
|
restarted_app = _make_app(
|
|
config,
|
|
repo,
|
|
runtime_config_store=ChannelRuntimeConfigStore(runtime_config_path),
|
|
set_channels_config_state=False,
|
|
)
|
|
|
|
with TestClient(restarted_app) as client:
|
|
response = client.get("/api/channels/providers")
|
|
|
|
assert response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in response.json()["providers"]}
|
|
assert by_provider["slack"]["configured"] is True
|
|
assert by_provider["slack"]["connectable"] is True
|
|
assert by_provider["slack"]["connection_status"] == "connected"
|
|
assert restarted_app.state.channels_config["slack"] == {
|
|
"enabled": True,
|
|
"bot_token": "xoxb-ui",
|
|
"app_token": "xapp-ui",
|
|
}
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_configure_provider_runtime_credentials_preserves_masked_secrets(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"feishu": {"enabled": True},
|
|
}
|
|
)
|
|
runtime_config_store = ChannelRuntimeConfigStore(tmp_path / "channels" / "runtime-config.json")
|
|
app = _make_app(
|
|
config,
|
|
repo,
|
|
{
|
|
"feishu": {
|
|
"enabled": True,
|
|
"app_id": "old-app-id",
|
|
"app_secret": "old-secret",
|
|
}
|
|
},
|
|
runtime_config_store=runtime_config_store,
|
|
)
|
|
|
|
with TestClient(app) as client:
|
|
configure_response = client.post(
|
|
"/api/channels/feishu/runtime-config",
|
|
json={
|
|
"values": {
|
|
"app_id": "new-app-id",
|
|
"app_secret": "********",
|
|
}
|
|
},
|
|
)
|
|
providers_response = client.get("/api/channels/providers")
|
|
|
|
assert configure_response.status_code == 200
|
|
assert app.state.channels_config["feishu"] == {
|
|
"enabled": True,
|
|
"app_id": "new-app-id",
|
|
"app_secret": "old-secret",
|
|
}
|
|
assert runtime_config_store.get_provider_config("feishu") == {
|
|
"enabled": True,
|
|
"app_id": "new-app-id",
|
|
"app_secret": "old-secret",
|
|
}
|
|
by_provider = {item["provider"]: item for item in providers_response.json()["providers"]}
|
|
assert by_provider["feishu"]["credential_values"] == {
|
|
"app_id": "new-app-id",
|
|
"app_secret": "********",
|
|
}
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_disconnect_provider_runtime_config_clears_connected_state(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"slack": {"enabled": True},
|
|
}
|
|
)
|
|
runtime_config_store = ChannelRuntimeConfigStore(tmp_path / "channels" / "runtime-config.json")
|
|
app = _make_app(config, repo, {}, runtime_config_store=runtime_config_store)
|
|
|
|
with TestClient(app) as client:
|
|
configure_response = client.post(
|
|
"/api/channels/slack/runtime-config",
|
|
json={"values": {"bot_token": "xoxb-ui", "app_token": "xapp-ui"}},
|
|
)
|
|
disconnect_response = client.delete("/api/channels/slack/runtime-config")
|
|
providers_response = client.get("/api/channels/providers")
|
|
|
|
assert configure_response.status_code == 200
|
|
assert disconnect_response.status_code == 200
|
|
disconnected = disconnect_response.json()
|
|
assert disconnected["provider"] == "slack"
|
|
assert disconnected["configured"] is False
|
|
assert disconnected["connectable"] is False
|
|
assert disconnected["connection_status"] == "not_connected"
|
|
assert runtime_config_store.get_provider_config("slack") == {
|
|
"enabled": False,
|
|
"_runtime_disabled": True,
|
|
}
|
|
|
|
assert providers_response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in providers_response.json()["providers"]}
|
|
assert by_provider["slack"]["connection_status"] == "not_connected"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_disconnect_provider_runtime_config_suppresses_file_config_and_stops_channel(tmp_path, monkeypatch):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"feishu": {"enabled": True},
|
|
}
|
|
)
|
|
set_app_config(
|
|
AppConfig.model_validate(
|
|
{
|
|
"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"},
|
|
"channels": {
|
|
"feishu": {
|
|
"enabled": True,
|
|
"app_id": "file-app-id",
|
|
"app_secret": "file-secret",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
)
|
|
runtime_config_store = ChannelRuntimeConfigStore(tmp_path / "channels" / "runtime-config.json")
|
|
runtime_config_store.set_provider_config(
|
|
"feishu",
|
|
{
|
|
"enabled": True,
|
|
"app_id": "runtime-app-id",
|
|
"app_secret": "runtime-secret",
|
|
},
|
|
)
|
|
service = SimpleNamespace(
|
|
configure_channel=AsyncMock(return_value=True),
|
|
remove_channel=AsyncMock(return_value=True),
|
|
)
|
|
monkeypatch.setattr("app.channels.service.get_channel_service", lambda: service)
|
|
app = _make_app(
|
|
config,
|
|
repo,
|
|
{
|
|
"feishu": {
|
|
"enabled": True,
|
|
"app_id": "runtime-app-id",
|
|
"app_secret": "runtime-secret",
|
|
}
|
|
},
|
|
runtime_config_store=runtime_config_store,
|
|
)
|
|
|
|
with TestClient(app) as client:
|
|
disconnect_response = client.delete("/api/channels/feishu/runtime-config")
|
|
providers_response = client.get("/api/channels/providers")
|
|
|
|
assert disconnect_response.status_code == 200
|
|
disconnected = disconnect_response.json()
|
|
assert disconnected["provider"] == "feishu"
|
|
assert disconnected["configured"] is False
|
|
assert disconnected["connectable"] is False
|
|
assert disconnected["connection_status"] == "not_connected"
|
|
assert "feishu" not in app.state.channels_config
|
|
service.remove_channel.assert_awaited_once_with("feishu")
|
|
service.configure_channel.assert_not_awaited()
|
|
|
|
assert providers_response.status_code == 200
|
|
by_provider = {item["provider"]: item for item in providers_response.json()["providers"]}
|
|
assert by_provider["feishu"]["configured"] is False
|
|
assert by_provider["feishu"]["connection_status"] == "not_connected"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_disconnect_provider_runtime_config_revokes_current_user_provider_connections(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
|
|
async def seed_connection():
|
|
await repo.upsert_connection(
|
|
owner_user_id=str(_user().id),
|
|
provider="slack",
|
|
external_account_id="U123",
|
|
status="connected",
|
|
)
|
|
|
|
anyio.run(seed_connection)
|
|
config = ChannelConnectionsConfig.model_validate(
|
|
{
|
|
"enabled": True,
|
|
"slack": {"enabled": True},
|
|
}
|
|
)
|
|
runtime_config_store = ChannelRuntimeConfigStore(tmp_path / "channels" / "runtime-config.json")
|
|
app = _make_app(config, repo, {}, runtime_config_store=runtime_config_store)
|
|
|
|
with TestClient(app) as client:
|
|
configure_response = client.post(
|
|
"/api/channels/slack/runtime-config",
|
|
json={"values": {"bot_token": "xoxb-ui", "app_token": "xapp-ui"}},
|
|
)
|
|
disconnect_response = client.delete("/api/channels/slack/runtime-config")
|
|
|
|
assert configure_response.status_code == 200
|
|
assert disconnect_response.status_code == 200
|
|
|
|
async def get_connection_status():
|
|
return (await repo.list_connections(str(_user().id)))[0]["status"]
|
|
|
|
assert anyio.run(get_connection_status) == "revoked"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_disconnect_connection_revokes_current_user_connection(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
|
|
async def seed_connection():
|
|
connection = await repo.upsert_connection(
|
|
owner_user_id=str(_user().id),
|
|
provider="telegram",
|
|
external_account_id="42",
|
|
status="connected",
|
|
)
|
|
return connection["id"]
|
|
|
|
connection_id = anyio.run(seed_connection)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.delete(f"/api/channels/connections/{connection_id}")
|
|
|
|
assert response.status_code == 204
|
|
|
|
async def get_connection_status():
|
|
return (await repo.list_connections(str(_user().id)))[0]["status"]
|
|
|
|
assert anyio.run(get_connection_status) == "revoked"
|
|
|
|
anyio.run(repo.close)
|
|
|
|
|
|
def test_disconnect_connection_is_current_user_scoped(tmp_path):
|
|
import anyio
|
|
|
|
repo = anyio.run(_make_repo, tmp_path)
|
|
|
|
async def seed_connection():
|
|
connection = await repo.upsert_connection(
|
|
owner_user_id="other-user",
|
|
provider="telegram",
|
|
external_account_id="42",
|
|
status="connected",
|
|
)
|
|
return connection["id"]
|
|
|
|
connection_id = anyio.run(seed_connection)
|
|
app = _make_app(_enabled_connections_config(), repo, _channels_config())
|
|
|
|
with TestClient(app) as client:
|
|
response = client.delete(f"/api/channels/connections/{connection_id}")
|
|
|
|
assert response.status_code == 404
|
|
|
|
async def get_connection_status():
|
|
return (await repo.list_connections("other-user"))[0]["status"]
|
|
|
|
assert anyio.run(get_connection_status) == "connected"
|
|
|
|
anyio.run(repo.close)
|