From e2110288666413f2ea0ef1b0a81de96b88e8d629 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 8 May 2026 13:12:34 +0530 Subject: [PATCH] test(e2e): add ClickUp MCP backend fake --- surfsense_backend/tests/e2e/fakes/chat_llm.py | 58 ++++++++ .../tests/e2e/fakes/clickup_module.py | 134 ++++++++++++++++++ .../e2e/fakes/fixtures/clickup_tasks.json | 13 ++ surfsense_backend/tests/e2e/run_backend.py | 2 + surfsense_backend/tests/e2e/run_celery.py | 2 + 5 files changed, 209 insertions(+) create mode 100644 surfsense_backend/tests/e2e/fakes/clickup_module.py create mode 100644 surfsense_backend/tests/e2e/fakes/fixtures/clickup_tasks.json diff --git a/surfsense_backend/tests/e2e/fakes/chat_llm.py b/surfsense_backend/tests/e2e/fakes/chat_llm.py index acfc0e77a..e0c6b7f1a 100644 --- a/surfsense_backend/tests/e2e/fakes/chat_llm.py +++ b/surfsense_backend/tests/e2e/fakes/chat_llm.py @@ -33,6 +33,9 @@ JIRA_CANARY_SUMMARY = "E2E Canary Jira Issue" JIRA_CANARY_KEY = "E2E-101" SLACK_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_SLACK_001" SLACK_CANARY_CHANNEL = "slack-e2e-canary" +CLICKUP_CANARY_TOKEN = "SURFSENSE_E2E_CANARY_TOKEN_CLICKUP_001" +CLICKUP_CANARY_TITLE = "E2E Canary ClickUp Task" +CLICKUP_CANARY_TASK_ID = "fake-clickup-task-canary-001" NO_RELEVANT_CONTENT_SENTINEL = "No relevant indexed content found." NO_RELEVANT_CONTENT_QUERY = "E2E_NO_RELEVANT_CONTENT_SMOKE" @@ -122,6 +125,11 @@ class FakeChatLLM(BaseChatModel): and SLACK_CANARY_TOKEN in latest_tool_text ): return f"Slack live tool content found: {SLACK_CANARY_TOKEN}" + if ( + latest_tool_name in {"clickup_search", "clickup_get_task"} + and CLICKUP_CANARY_TOKEN in latest_tool_text + ): + return f"ClickUp live tool content found: {CLICKUP_CANARY_TOKEN}" wants_gmail = _contains_any( latest_human, @@ -170,6 +178,10 @@ class FakeChatLLM(BaseChatModel): latest_human, ("slack", SLACK_CANARY_TOKEN), ) + wants_clickup = _contains_any( + latest_human, + ("clickup", CLICKUP_CANARY_TITLE), + ) has_gmail_evidence = ( GMAIL_CANARY_SUBJECT in prompt_text or GMAIL_CANARY_MESSAGE_ID in prompt_text @@ -222,7 +234,14 @@ class FakeChatLLM(BaseChatModel): or "C_FAKE_SLACK_CANARY" in prompt_text or "T_FAKE_SLACK_TEAM" in prompt_text ) + has_clickup_evidence = ( + CLICKUP_CANARY_TITLE in prompt_text + or CLICKUP_CANARY_TOKEN in prompt_text + or CLICKUP_CANARY_TASK_ID in prompt_text + ) + if wants_clickup and has_clickup_evidence: + return f"ClickUp content found: {CLICKUP_CANARY_TOKEN}" if wants_slack and has_slack_evidence: return f"Slack content found: {SLACK_CANARY_TOKEN}" if wants_jira and has_jira_evidence: @@ -254,6 +273,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Notion content found: {NOTION_CANARY_TOKEN}" if ( @@ -267,6 +287,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Confluence content found: {CONFLUENCE_CANARY_TOKEN}" if ( @@ -280,6 +301,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Jira content found: {JIRA_CANARY_TOKEN}" if ( @@ -293,6 +315,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Linear content found: {LINEAR_CANARY_TOKEN}" if ( @@ -306,6 +329,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Calendar content found: {CALENDAR_CANARY_TOKEN}" if ( @@ -318,6 +342,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Gmail content found: {GMAIL_CANARY_TOKEN}" if ( @@ -331,6 +356,7 @@ class FakeChatLLM(BaseChatModel): and not has_drive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"OneDrive content found: {ONEDRIVE_CANARY_TOKEN}" if ( @@ -344,6 +370,7 @@ class FakeChatLLM(BaseChatModel): and not has_drive_evidence and not has_onedrive_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Dropbox content found: {DROPBOX_CANARY_TOKEN}" if ( @@ -356,6 +383,7 @@ class FakeChatLLM(BaseChatModel): and not has_onedrive_evidence and not has_dropbox_evidence and not has_slack_evidence + and not has_clickup_evidence ): return f"Drive content found: {DRIVE_CANARY_TOKEN}" if ( @@ -369,8 +397,23 @@ class FakeChatLLM(BaseChatModel): and not has_drive_evidence and not has_onedrive_evidence and not has_dropbox_evidence + and not has_clickup_evidence ): return f"Slack content found: {SLACK_CANARY_TOKEN}" + if ( + has_clickup_evidence + and not has_confluence_evidence + and not has_jira_evidence + and not has_linear_evidence + and not has_notion_evidence + and not has_calendar_evidence + and not has_gmail_evidence + and not has_drive_evidence + and not has_onedrive_evidence + and not has_dropbox_evidence + and not has_slack_evidence + ): + return f"ClickUp content found: {CLICKUP_CANARY_TOKEN}" return NO_RELEVANT_CONTENT_SENTINEL def _tool_call_message_for(self, messages: list[BaseMessage]) -> AIMessage | None: @@ -493,6 +536,21 @@ class FakeChatLLM(BaseChatModel): ], ) + if latest_tool is None and _contains_any( + latest_human, + ("clickup", CLICKUP_CANARY_TITLE), + ): + return AIMessage( + content="", + tool_calls=[ + { + "name": "clickup_search", + "args": {"query": CLICKUP_CANARY_TITLE, "limit": 5}, + "id": "call_e2e_search_clickup_tasks", + } + ], + ) + return None def _generate( diff --git a/surfsense_backend/tests/e2e/fakes/clickup_module.py b/surfsense_backend/tests/e2e/fakes/clickup_module.py new file mode 100644 index 000000000..b847ac73f --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/clickup_module.py @@ -0,0 +1,134 @@ +"""Strict ClickUp 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 tests.e2e.fakes import mcp_oauth_runtime, mcp_runtime + +_FIXTURE_PATH = Path(__file__).parent / "fixtures" / "clickup_tasks.json" + +_AUTHORIZATION_URL = "https://mcp.clickup.com/authorize" +_REGISTRATION_URL = "https://mcp.clickup.com/register" +_TOKEN_URL = "https://mcp.clickup.com/token" +_MCP_URL = "https://mcp.clickup.com/mcp" + +_CLIENT_ID = "fake-clickup-mcp-client-id" +_CLIENT_SECRET = "fake-clickup-mcp-client-secret" +_ACCESS_TOKEN = "fake-clickup-mcp-access-token" +_REFRESH_TOKEN = "fake-clickup-mcp-refresh-token" +_OAUTH_CODE = "fake-clickup-oauth-code" + + +def _load_fixture() -> dict[str, Any]: + with _FIXTURE_PATH.open() as f: + return json.load(f) + + +_FIXTURE = _load_fixture() + + +def _task_text(task: dict[str, Any]) -> str: + return ( + f"{task['name']}\n" + f"id: {task['id']}\n" + f"workspace: {task['workspace_name']} ({task['workspace_id']})\n" + f"list: {task['list_name']}\n" + f"status: {task['status']}\n" + f"description: {task['description']}" + ) + + +async def _list_tools() -> SimpleNamespace: + return SimpleNamespace( + tools=[ + SimpleNamespace( + name="clickup_search", + description="Search ClickUp tasks visible to the authenticated user.", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Text to search for in ClickUp tasks.", + }, + "limit": { + "type": "integer", + "description": "Maximum number of tasks to return.", + }, + }, + "required": ["query"], + }, + ), + SimpleNamespace( + name="clickup_get_task", + description="Get a ClickUp task by id.", + inputSchema={ + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "ClickUp task id.", + } + }, + "required": ["task_id"], + }, + ), + ] + ) + + +async def _call_tool( + tool_name: str, arguments: dict[str, Any] | None = None +) -> SimpleNamespace: + arguments = arguments or {} + task = _FIXTURE["tasks"][0] + + if tool_name == "clickup_search": + query = str(arguments.get("query", "")) + if query and task["name"].lower() not in query.lower(): + raise ValueError(f"Unexpected ClickUp task query: {query!r}") + return SimpleNamespace(content=[SimpleNamespace(text=_task_text(task))]) + + if tool_name == "clickup_get_task": + task_id = arguments.get("task_id") + if task_id != task["id"]: + raise ValueError(f"Unexpected ClickUp task id: {task_id!r}") + return SimpleNamespace(content=[SimpleNamespace(text=_task_text(task))]) + + raise NotImplementedError(f"Unexpected ClickUp MCP tool call: {tool_name!r}") + + +def install(active_patches: list[Any]) -> None: + """Register ClickUp MCP OAuth/tool handlers with the shared dispatchers.""" + del active_patches + mcp_oauth_runtime.register_service( + mcp_url=_MCP_URL, + discovery_metadata={ + "issuer": "https://mcp.clickup.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="read write", + redirect_uri_substring="/api/v1/auth/mcp/clickup/connector/callback", + ) + mcp_runtime.register( + url=_MCP_URL, + expected_bearer=_ACCESS_TOKEN, + list_tools=_list_tools, + call_tool=_call_tool, + ) diff --git a/surfsense_backend/tests/e2e/fakes/fixtures/clickup_tasks.json b/surfsense_backend/tests/e2e/fakes/fixtures/clickup_tasks.json new file mode 100644 index 000000000..1c49b41b6 --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/fixtures/clickup_tasks.json @@ -0,0 +1,13 @@ +{ + "tasks": [ + { + "id": "fake-clickup-task-canary-001", + "name": "E2E Canary ClickUp Task", + "list_name": "SurfSense E2E ClickUp List", + "workspace_id": "fake-clickup-workspace-001", + "workspace_name": "SurfSense E2E ClickUp Workspace", + "status": "open", + "description": "Canary task body containing SURFSENSE_E2E_CANARY_TOKEN_CLICKUP_001" + } + ] +} diff --git a/surfsense_backend/tests/e2e/run_backend.py b/surfsense_backend/tests/e2e/run_backend.py index 9c880e0a8..8f0658037 100644 --- a/surfsense_backend/tests/e2e/run_backend.py +++ b/surfsense_backend/tests/e2e/run_backend.py @@ -109,6 +109,7 @@ from unittest.mock import patch # noqa: E402 from app.app import app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 + clickup_module as _fake_clickup_module, confluence_indexer as _fake_confluence_indexer, confluence_oauth as _fake_confluence_oauth, dropbox_api as _fake_dropbox_api, @@ -200,6 +201,7 @@ _fake_dropbox_api.install(_active_patches) _fake_notion_module.install(_active_patches) _fake_linear_module.install(_active_patches) _fake_jira_module.install(_active_patches) +_fake_clickup_module.install(_active_patches) _fake_mcp_runtime.install(_active_patches) _fake_mcp_oauth_runtime.install(_active_patches) _fake_slack_module.install(_active_patches) diff --git a/surfsense_backend/tests/e2e/run_celery.py b/surfsense_backend/tests/e2e/run_celery.py index 7b084e0ac..1d75c20dc 100644 --- a/surfsense_backend/tests/e2e/run_celery.py +++ b/surfsense_backend/tests/e2e/run_celery.py @@ -94,6 +94,7 @@ from unittest.mock import patch # noqa: E402 from app.celery_app import celery_app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 + clickup_module as _fake_clickup_module, confluence_indexer as _fake_confluence_indexer, confluence_oauth as _fake_confluence_oauth, dropbox_api as _fake_dropbox_api, @@ -184,6 +185,7 @@ _fake_dropbox_api.install(_active_patches) _fake_notion_module.install(_active_patches) _fake_linear_module.install(_active_patches) _fake_jira_module.install(_active_patches) +_fake_clickup_module.install(_active_patches) _fake_mcp_runtime.install(_active_patches) _fake_mcp_oauth_runtime.install(_active_patches) _fake_slack_module.install(_active_patches)