mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
test(e2e): add Slack MCP backend fake
This commit is contained in:
parent
ef843fd82e
commit
bf23cb2381
2 changed files with 231 additions and 0 deletions
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"team": {
|
||||
"id": "T_FAKE_SLACK_TEAM",
|
||||
"name": "SurfSense E2E Slack Workspace"
|
||||
},
|
||||
"channel": {
|
||||
"id": "C_FAKE_SLACK_CANARY",
|
||||
"name": "e2e-canary",
|
||||
"purpose": "SurfSense E2E Slack canary channel"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"ts": "1715000000.000100",
|
||||
"user": "U_FAKE_SLACK_USER",
|
||||
"text": "This Slack message proves the live MCP tool fetched channel content. SURFSENSE_E2E_CANARY_TOKEN_SLACK_001"
|
||||
}
|
||||
]
|
||||
}
|
||||
213
surfsense_backend/tests/e2e/fakes/slack_module.py
Normal file
213
surfsense_backend/tests/e2e/fakes/slack_module.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
"""Strict Slack MCP OAuth/tool fakes for Playwright E2E."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.e2e.fakes import mcp_oauth_runtime, mcp_runtime
|
||||
|
||||
_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "slack_messages.json"
|
||||
|
||||
_AUTHORIZATION_URL = "https://slack.com/oauth/v2_user/authorize"
|
||||
_REGISTRATION_URL = "https://e2e-fake.invalid/mcp/slack-unused-register"
|
||||
_TOKEN_URL = "https://slack.com/api/oauth.v2.user.access"
|
||||
_MCP_URL = "https://mcp.slack.com/mcp"
|
||||
|
||||
_CLIENT_ID = "fake-slack-mcp-client-id"
|
||||
_CLIENT_SECRET = "fake-slack-mcp-client-secret"
|
||||
_ACCESS_TOKEN = "fake-slack-mcp-access-token"
|
||||
_REFRESH_TOKEN = "fake-slack-mcp-refresh-token"
|
||||
_OAUTH_CODE = "fake-slack-oauth-code"
|
||||
_SCOPE = (
|
||||
"search:read.public search:read.private search:read.mpim search:read.im "
|
||||
"channels:history groups:history mpim:history im:history"
|
||||
)
|
||||
|
||||
|
||||
def _load_fixture() -> dict[str, Any]:
|
||||
with _FIXTURE_PATH.open() as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
_FIXTURE = _load_fixture()
|
||||
|
||||
|
||||
async def _list_tools() -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
tools=[
|
||||
SimpleNamespace(
|
||||
name="slack_search_channels",
|
||||
description="Search Slack channels visible to the authenticated user.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Text to search for in Slack channel names.",
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of channels to return.",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
),
|
||||
SimpleNamespace(
|
||||
name="slack_read_channel",
|
||||
description="Read messages from a Slack channel.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel_id": {
|
||||
"type": "string",
|
||||
"description": "Slack channel id.",
|
||||
}
|
||||
},
|
||||
"required": ["channel_id"],
|
||||
},
|
||||
),
|
||||
SimpleNamespace(
|
||||
name="slack_read_thread",
|
||||
description="Read a Slack thread from a channel.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"channel_id": {
|
||||
"type": "string",
|
||||
"description": "Slack channel id.",
|
||||
},
|
||||
"thread_ts": {
|
||||
"type": "string",
|
||||
"description": "Slack thread timestamp.",
|
||||
},
|
||||
},
|
||||
"required": ["channel_id", "thread_ts"],
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def _call_tool(
|
||||
tool_name: str, arguments: dict[str, Any] | None = None
|
||||
) -> SimpleNamespace:
|
||||
arguments = arguments or {}
|
||||
channel = _FIXTURE["channel"]
|
||||
message = _FIXTURE["messages"][0]
|
||||
|
||||
if tool_name == "slack_search_channels":
|
||||
query = str(arguments.get("query", ""))
|
||||
if query and channel["name"].lower() not in query.lower():
|
||||
raise ValueError(f"Unexpected Slack channel query: {query!r}")
|
||||
text = (
|
||||
f"#{channel['name']} ({channel['id']})\n"
|
||||
f"purpose: {channel['purpose']}\n"
|
||||
f"latest_message: {message['text']}"
|
||||
)
|
||||
return SimpleNamespace(content=[SimpleNamespace(text=text)])
|
||||
|
||||
if tool_name in {"slack_read_channel", "slack_read_thread"}:
|
||||
raise NotImplementedError(
|
||||
f"Slack E2E fake does not exercise {tool_name!r}; "
|
||||
"extend slack_module.py before using it in a journey."
|
||||
)
|
||||
|
||||
raise NotImplementedError(f"Unexpected Slack MCP tool call: {tool_name!r}")
|
||||
|
||||
|
||||
async def _fake_exchange_code_for_tokens(
|
||||
token_endpoint: str,
|
||||
code: str,
|
||||
redirect_uri: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
code_verifier: str,
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
) -> dict[str, Any]:
|
||||
if token_endpoint != _TOKEN_URL:
|
||||
return await mcp_oauth_runtime._fake_exchange_code_for_tokens( # noqa: SLF001
|
||||
token_endpoint,
|
||||
code,
|
||||
redirect_uri,
|
||||
client_id,
|
||||
client_secret,
|
||||
code_verifier,
|
||||
timeout=timeout,
|
||||
)
|
||||
del timeout
|
||||
|
||||
if code != _OAUTH_CODE:
|
||||
raise ValueError(f"Unexpected fake Slack OAuth code: {code!r}")
|
||||
if "/api/v1/auth/mcp/slack/connector/callback" not in redirect_uri:
|
||||
raise ValueError(f"Unexpected Slack redirect_uri={redirect_uri!r}")
|
||||
if client_id != _CLIENT_ID or client_secret != _CLIENT_SECRET:
|
||||
raise ValueError(
|
||||
"Unexpected Slack client credentials: "
|
||||
f"client_id={client_id!r} client_secret={client_secret!r}"
|
||||
)
|
||||
if not code_verifier:
|
||||
raise ValueError("Slack token exchange missing code_verifier.")
|
||||
|
||||
team = _FIXTURE["team"]
|
||||
return {
|
||||
"ok": True,
|
||||
"scope": _SCOPE,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": _REFRESH_TOKEN,
|
||||
"authed_user": {
|
||||
"id": "U_FAKE_SLACK_USER",
|
||||
"scope": _SCOPE,
|
||||
"access_token": _ACCESS_TOKEN,
|
||||
"refresh_token": _REFRESH_TOKEN,
|
||||
"expires_in": 3600,
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
"team": {
|
||||
"id": team["id"],
|
||||
"name": team["name"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def install(active_patches: list[Any]) -> None:
|
||||
"""Register Slack MCP OAuth/tool handlers with the shared dispatchers."""
|
||||
mcp_oauth_runtime.register_service(
|
||||
mcp_url=_MCP_URL,
|
||||
discovery_metadata={
|
||||
"issuer": "https://slack.com",
|
||||
"authorization_endpoint": _AUTHORIZATION_URL,
|
||||
"token_endpoint": _TOKEN_URL,
|
||||
"registration_endpoint": _REGISTRATION_URL,
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"response_types_supported": ["code"],
|
||||
},
|
||||
client_id=_CLIENT_ID,
|
||||
client_secret=_CLIENT_SECRET,
|
||||
token_endpoint=_TOKEN_URL,
|
||||
registration_endpoint=_REGISTRATION_URL,
|
||||
oauth_code=_OAUTH_CODE,
|
||||
access_token=_ACCESS_TOKEN,
|
||||
refresh_token=_REFRESH_TOKEN,
|
||||
scope=_SCOPE,
|
||||
redirect_uri_substring="/api/v1/auth/mcp/slack/connector/callback",
|
||||
)
|
||||
mcp_runtime.register(
|
||||
url=_MCP_URL,
|
||||
expected_bearer=_ACCESS_TOKEN,
|
||||
list_tools=_list_tools,
|
||||
call_tool=_call_tool,
|
||||
)
|
||||
p = patch(
|
||||
"app.services.mcp_oauth.discovery.exchange_code_for_tokens",
|
||||
_fake_exchange_code_for_tokens,
|
||||
)
|
||||
p.start()
|
||||
active_patches.append(p)
|
||||
Loading…
Add table
Add a link
Reference in a new issue