From 369f0aaff34c644a5ac7e212fa40c803e93b25d7 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 8 May 2026 00:15:08 +0530 Subject: [PATCH] test(backend): add Jira MCP E2E fakes --- .../tests/e2e/fakes/fixtures/jira_issues.json | 15 ++ .../tests/e2e/fakes/jira_module.py | 136 ++++++++++++++++++ .../tests/e2e/fakes/linear_module.py | 67 +++++---- surfsense_backend/tests/e2e/run_backend.py | 6 + surfsense_backend/tests/e2e/run_celery.py | 8 ++ 5 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 surfsense_backend/tests/e2e/fakes/fixtures/jira_issues.json create mode 100644 surfsense_backend/tests/e2e/fakes/jira_module.py diff --git a/surfsense_backend/tests/e2e/fakes/fixtures/jira_issues.json b/surfsense_backend/tests/e2e/fakes/fixtures/jira_issues.json new file mode 100644 index 000000000..3acea1993 --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/fixtures/jira_issues.json @@ -0,0 +1,15 @@ +{ + "site": { + "cloud_id": "fake-jira-cloud-001", + "name": "SurfSense E2E Atlassian", + "url": "https://surfsense-e2e.atlassian.net" + }, + "issues": [ + { + "id": "fake-jira-issue-canary-001", + "key": "E2E-101", + "summary": "E2E Canary Jira Issue", + "description": "This Jira issue proves live MCP tool calls work end-to-end. SURFSENSE_E2E_CANARY_TOKEN_JIRA_001" + } + ] +} diff --git a/surfsense_backend/tests/e2e/fakes/jira_module.py b/surfsense_backend/tests/e2e/fakes/jira_module.py new file mode 100644 index 000000000..234d0ff44 --- /dev/null +++ b/surfsense_backend/tests/e2e/fakes/jira_module.py @@ -0,0 +1,136 @@ +"""Strict Jira 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" / "jira_issues.json" + +_AUTHORIZATION_URL = "https://mcp.atlassian.com/v1/authorize" +_REGISTRATION_URL = "https://cf.mcp.atlassian.com/v1/register" +_TOKEN_URL = "https://cf.mcp.atlassian.com/v1/token" +_MCP_URL = "https://mcp.atlassian.com/v1/mcp" + +_CLIENT_ID = "fake-jira-mcp-client-id" +_CLIENT_SECRET = "fake-jira-mcp-client-secret" +_ACCESS_TOKEN = "fake-jira-mcp-access-token" +_REFRESH_TOKEN = "fake-jira-mcp-refresh-token" +_OAUTH_CODE = "fake-jira-oauth-code" + + +def _load_fixture() -> dict[str, Any]: + with _FIXTURE_PATH.open() as f: + return json.load(f) + + +_FIXTURE = _load_fixture() + + +def _issue_text(issue: dict[str, Any]) -> str: + return ( + f"{issue['key']} {issue['summary']}\n" + f"id: {issue['id']}\n" + f"description: {issue['description']}" + ) + + +async def _list_tools() -> SimpleNamespace: + return SimpleNamespace( + tools=[ + SimpleNamespace( + name="getAccessibleAtlassianResources", + description="Get Jira sites accessible to the authenticated Atlassian user.", + inputSchema={ + "type": "object", + "properties": {}, + "required": [], + }, + ), + SimpleNamespace( + name="searchJiraIssuesUsingJql", + description="Search Jira issues using a Jira Query Language expression.", + inputSchema={ + "type": "object", + "properties": { + "jql": { + "type": "string", + "description": "JQL query used to search Jira issues.", + }, + "maxResults": { + "type": "integer", + "description": "Maximum number of matching issues to return.", + }, + }, + "required": ["jql"], + }, + ), + ] + ) + + +async def _call_tool( + tool_name: str, arguments: dict[str, Any] | None = None +) -> SimpleNamespace: + arguments = arguments or {} + site = _FIXTURE["site"] + issue = _FIXTURE["issues"][0] + + if tool_name == "getAccessibleAtlassianResources": + if arguments: + raise ValueError( + f"Unexpected Jira getAccessibleAtlassianResources args: {arguments!r}" + ) + text = ( + f"{site['name']}\n" + f"cloud_id: {site['cloud_id']}\n" + f"url: {site['url']}" + ) + return SimpleNamespace(content=[SimpleNamespace(text=text)]) + + if tool_name == "searchJiraIssuesUsingJql": + jql = str(arguments.get("jql", "")) + if issue["summary"].lower() not in jql.lower() and issue[ + "key" + ].lower() not in jql.lower(): + raise ValueError(f"Unexpected Jira JQL query: {jql!r}") + text = _issue_text(issue) + return SimpleNamespace(content=[SimpleNamespace(text=text)]) + + raise NotImplementedError(f"Unexpected Jira MCP tool call: {tool_name!r}") + + +def install(active_patches: list[Any]) -> None: + """Register Jira 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.atlassian.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:jira-work read:me write:jira-work", + redirect_uri_substring="/api/v1/auth/mcp/jira/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/linear_module.py b/surfsense_backend/tests/e2e/fakes/linear_module.py index be3a0f041..b6eb48242 100644 --- a/surfsense_backend/tests/e2e/fakes/linear_module.py +++ b/surfsense_backend/tests/e2e/fakes/linear_module.py @@ -6,7 +6,8 @@ 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" / "linear_issues.json" @@ -262,29 +263,43 @@ def _fake_streamablehttp_client( return _FakeStreamableHttpClient(url, headers=headers, **kwargs) +async def _list_tools() -> SimpleNamespace: + return await _FakeClientSession(object(), object()).list_tools() + + +async def _call_tool(tool_name: str, arguments: dict[str, Any]) -> SimpleNamespace: + return await _FakeClientSession(object(), object()).call_tool( + tool_name, arguments=arguments + ) + + def install(active_patches: list[Any]) -> None: - """Patch production Linear MCP OAuth/tool boundaries.""" - targets = [ - ( - "app.services.mcp_oauth.discovery.discover_oauth_metadata", - _fake_discover_oauth_metadata, - ), - ("app.services.mcp_oauth.discovery.register_client", _fake_register_client), - ( - "app.services.mcp_oauth.discovery.exchange_code_for_tokens", - _fake_exchange_code_for_tokens, - ), - ( - "app.services.mcp_oauth.discovery.refresh_access_token", - _fake_refresh_access_token, - ), - ( - "app.agents.new_chat.tools.mcp_tool.streamablehttp_client", - _fake_streamablehttp_client, - ), - ("app.agents.new_chat.tools.mcp_tool.ClientSession", _FakeClientSession), - ] - for target, replacement in targets: - p = patch(target, replacement) - p.start() - active_patches.append(p) + """Register Linear 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.linear.app", + "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/linear/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/run_backend.py b/surfsense_backend/tests/e2e/run_backend.py index 21ce1247a..7158811aa 100644 --- a/surfsense_backend/tests/e2e/run_backend.py +++ b/surfsense_backend/tests/e2e/run_backend.py @@ -90,7 +90,10 @@ from unittest.mock import patch # noqa: E402 from app.app import app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 embeddings as _fake_embeddings, + jira_module as _fake_jira_module, linear_module as _fake_linear_module, + mcp_oauth_runtime as _fake_mcp_oauth_runtime, + mcp_runtime as _fake_mcp_runtime, native_google as _fake_native_google, notion_module as _fake_notion_module, ) @@ -164,6 +167,9 @@ _fake_embeddings.install(_active_patches) _fake_native_google.install(_active_patches) _fake_notion_module.install(_active_patches) _fake_linear_module.install(_active_patches) +_fake_jira_module.install(_active_patches) +_fake_mcp_runtime.install(_active_patches) +_fake_mcp_oauth_runtime.install(_active_patches) # --------------------------------------------------------------------------- diff --git a/surfsense_backend/tests/e2e/run_celery.py b/surfsense_backend/tests/e2e/run_celery.py index c20c44cb6..1794bc569 100644 --- a/surfsense_backend/tests/e2e/run_celery.py +++ b/surfsense_backend/tests/e2e/run_celery.py @@ -75,6 +75,10 @@ from unittest.mock import patch # noqa: E402 from app.celery_app import celery_app # noqa: E402 from tests.e2e.fakes import ( # noqa: E402 embeddings as _fake_embeddings, + jira_module as _fake_jira_module, + linear_module as _fake_linear_module, + mcp_oauth_runtime as _fake_mcp_oauth_runtime, + mcp_runtime as _fake_mcp_runtime, native_google as _fake_native_google, notion_module as _fake_notion_module, ) @@ -146,6 +150,10 @@ _patch_llm_bindings() _fake_embeddings.install(_active_patches) _fake_native_google.install(_active_patches) _fake_notion_module.install(_active_patches) +_fake_linear_module.install(_active_patches) +_fake_jira_module.install(_active_patches) +_fake_mcp_runtime.install(_active_patches) +_fake_mcp_oauth_runtime.install(_active_patches) # ---------------------------------------------------------------------------