mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 09:55:59 +00:00
Add user-owned IM channel connections
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""Slack OAuth Events tests for user-owned channel connections."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from uuid import UUID
|
||||
|
||||
from _router_auth_helpers import make_authed_test_app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.channels.message_bus import MessageBus, OutboundMessage
|
||||
from app.channels.providers.slack_connect import verify_slack_signature
|
||||
from app.gateway.auth.models import User
|
||||
from app.gateway.routers import channel_connections
|
||||
from deerflow.config.channel_connections_config import ChannelConnectionsConfig
|
||||
|
||||
|
||||
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, ChannelCredentialCipher
|
||||
from deerflow.persistence.engine import get_session_factory, init_engine
|
||||
|
||||
await init_engine("sqlite", url=f"sqlite+aiosqlite:///{tmp_path / 'slack.db'}", sqlite_dir=str(tmp_path))
|
||||
return ChannelConnectionRepository(
|
||||
get_session_factory(),
|
||||
cipher=ChannelCredentialCipher.from_key("slack-secret"),
|
||||
)
|
||||
|
||||
|
||||
def _make_app(config: ChannelConnectionsConfig, repo, bus):
|
||||
app = make_authed_test_app(user_factory=_user)
|
||||
app.state.channel_connections_config = config
|
||||
app.state.channel_connection_repo = repo
|
||||
app.state.channel_message_bus = bus
|
||||
app.include_router(channel_connections.router)
|
||||
return app
|
||||
|
||||
|
||||
def _slack_signature(signing_secret: str, timestamp: str, body: bytes) -> str:
|
||||
base = f"v0:{timestamp}:".encode() + body
|
||||
digest = hmac.new(signing_secret.encode("utf-8"), base, hashlib.sha256).hexdigest()
|
||||
return f"v0={digest}"
|
||||
|
||||
|
||||
def test_verify_slack_signature_accepts_valid_signature():
|
||||
body = b'{"type":"event_callback"}'
|
||||
timestamp = "1710000000"
|
||||
signature = _slack_signature("secret", timestamp, body)
|
||||
|
||||
assert verify_slack_signature(
|
||||
signing_secret="secret",
|
||||
timestamp=timestamp,
|
||||
body=body,
|
||||
signature=signature,
|
||||
now=1710000001,
|
||||
)
|
||||
|
||||
|
||||
def test_verify_slack_signature_rejects_stale_timestamp():
|
||||
body = b'{"type":"event_callback"}'
|
||||
timestamp = "1710000000"
|
||||
signature = _slack_signature("secret", timestamp, body)
|
||||
|
||||
assert not verify_slack_signature(
|
||||
signing_secret="secret",
|
||||
timestamp=timestamp,
|
||||
body=body,
|
||||
signature=signature,
|
||||
now=1710001000,
|
||||
)
|
||||
|
||||
|
||||
def test_slack_events_webhook_publishes_connection_scoped_inbound(tmp_path):
|
||||
import anyio
|
||||
|
||||
repo = anyio.run(_make_repo, tmp_path)
|
||||
|
||||
async def seed_connection():
|
||||
return await repo.upsert_connection(
|
||||
owner_user_id=str(_user().id),
|
||||
provider="slack",
|
||||
external_account_id="U123",
|
||||
workspace_id="T123",
|
||||
workspace_name="Deer Team",
|
||||
status="connected",
|
||||
)
|
||||
|
||||
connection = anyio.run(seed_connection)
|
||||
bus = AsyncMock()
|
||||
app = _make_app(
|
||||
ChannelConnectionsConfig.model_validate(
|
||||
{
|
||||
"enabled": True,
|
||||
"public_base_url": "https://deerflow.example.com",
|
||||
"encryption_key": "slack-secret",
|
||||
"slack": {
|
||||
"enabled": True,
|
||||
"client_id": "slack-client",
|
||||
"client_secret": "slack-secret",
|
||||
"signing_secret": "slack-signing-secret",
|
||||
},
|
||||
}
|
||||
),
|
||||
repo,
|
||||
bus,
|
||||
)
|
||||
payload = {
|
||||
"type": "event_callback",
|
||||
"event_id": "Ev123",
|
||||
"team_id": "T123",
|
||||
"event": {
|
||||
"type": "app_mention",
|
||||
"user": "U123",
|
||||
"channel": "C123",
|
||||
"text": "hello deerflow",
|
||||
"ts": "1710000000.000100",
|
||||
},
|
||||
}
|
||||
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||
timestamp = str(int(time.time()))
|
||||
headers = {
|
||||
"X-Slack-Request-Timestamp": timestamp,
|
||||
"X-Slack-Signature": _slack_signature("slack-signing-secret", timestamp, body),
|
||||
}
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/api/channels/webhooks/slack/events", content=body, headers=headers)
|
||||
duplicate = client.post("/api/channels/webhooks/slack/events", content=body, headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"ok": True, "processed": True}
|
||||
assert duplicate.status_code == 200
|
||||
assert duplicate.json() == {"ok": True, "duplicate": True, "processed": False}
|
||||
bus.publish_inbound.assert_awaited_once()
|
||||
inbound = bus.publish_inbound.call_args.args[0]
|
||||
assert inbound.connection_id == connection["id"]
|
||||
assert inbound.owner_user_id == str(_user().id)
|
||||
assert inbound.workspace_id == "T123"
|
||||
assert inbound.chat_id == "C123"
|
||||
assert inbound.user_id == "U123"
|
||||
assert inbound.text == "hello deerflow"
|
||||
assert inbound.topic_id == "1710000000.000100"
|
||||
|
||||
anyio.run(repo.close)
|
||||
|
||||
|
||||
def test_slack_send_uses_connection_bot_token_when_connection_id_is_present():
|
||||
import anyio
|
||||
|
||||
from app.channels.slack import SlackChannel
|
||||
|
||||
async def go():
|
||||
repo = AsyncMock()
|
||||
repo.get_credentials.return_value = {"access_token": "xoxb-connection-token"}
|
||||
web_client = MagicMock()
|
||||
web_client_factory = MagicMock(return_value=web_client)
|
||||
channel = SlackChannel(
|
||||
bus=MessageBus(),
|
||||
config={
|
||||
"connection_repo": repo,
|
||||
"web_client_factory": web_client_factory,
|
||||
},
|
||||
)
|
||||
|
||||
msg = OutboundMessage(
|
||||
channel_name="slack",
|
||||
chat_id="C123",
|
||||
thread_id="thread-1",
|
||||
text="hello",
|
||||
connection_id="connection-1",
|
||||
)
|
||||
await channel.send(msg)
|
||||
|
||||
repo.get_credentials.assert_awaited_once_with("connection-1")
|
||||
web_client_factory.assert_called_once_with(token="xoxb-connection-token")
|
||||
web_client.chat_postMessage.assert_called_once()
|
||||
|
||||
anyio.run(go)
|
||||
Reference in New Issue
Block a user