Skip to main content
Glama

MCP Atlassian

by ArconixForge
test_cross_service.py25 kB
"""Integration tests for cross-service functionality between Jira and Confluence.""" import os from unittest.mock import MagicMock, patch import pytest from requests.sessions import Session from mcp_atlassian.confluence import ConfluenceConfig from mcp_atlassian.jira import JiraConfig from mcp_atlassian.servers.context import MainAppContext from mcp_atlassian.servers.dependencies import ( _create_user_config_for_fetcher, get_confluence_fetcher, get_jira_fetcher, ) from mcp_atlassian.servers.main import AtlassianMCP, main_lifespan from mcp_atlassian.utils.environment import get_available_services from mcp_atlassian.utils.ssl import configure_ssl_verification from tests.utils.factories import ( ConfluencePageFactory, JiraIssueFactory, ) from tests.utils.mocks import MockAtlassianClient, MockEnvironment, MockFastMCP @pytest.mark.integration class TestCrossServiceUserResolution: """Test user resolution across Jira and Confluence services.""" def test_shared_user_email_resolution(self): """Test that user email is resolved consistently across services.""" user_email = "test@example.com" # Create mock clients jira_client = MockAtlassianClient.create_jira_client() confluence_client = MockAtlassianClient.create_confluence_client() # Mock user resolution jira_client.user.return_value = { "emailAddress": user_email, "displayName": "Test User", "accountId": "123456", } confluence_client.get_user.return_value = { "email": user_email, "displayName": "Test User", "accountId": "123456", } # Verify consistent user resolution jira_user = jira_client.user("123456") confluence_user = confluence_client.get_user("123456") assert jira_user["emailAddress"] == confluence_user["email"] assert jira_user["displayName"] == confluence_user["displayName"] assert jira_user["accountId"] == confluence_user["accountId"] @pytest.mark.anyio async def test_user_context_propagation(self): """Test that user context is properly propagated between services.""" with MockEnvironment.oauth_env() as env: # Create configurations jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() # Create user-specific configurations user_token = "test-user-token" user_email = "test@example.com" credentials = { "user_email_context": user_email, "oauth_access_token": user_token, } # Create user configs for both services user_jira_config = _create_user_config_for_fetcher( base_config=jira_config, auth_type="oauth", credentials=credentials ) user_confluence_config = _create_user_config_for_fetcher( base_config=confluence_config, auth_type="oauth", credentials=credentials, ) # Verify consistent OAuth configuration assert user_jira_config.oauth_config.access_token == user_token assert user_confluence_config.oauth_config.access_token == user_token assert user_jira_config.username == user_email assert user_confluence_config.username == user_email @pytest.mark.integration class TestSharedAuthentication: """Test shared authentication context between services.""" def test_oauth_shared_configuration(self): """Test that OAuth configuration is shared between services.""" with MockEnvironment.oauth_env() as env: # Both services should use the same OAuth configuration jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() assert ( jira_config.oauth_config.client_id == confluence_config.oauth_config.client_id ) assert ( jira_config.oauth_config.client_secret == confluence_config.oauth_config.client_secret ) assert ( jira_config.oauth_config.cloud_id == confluence_config.oauth_config.cloud_id ) assert ( jira_config.oauth_config.scope == confluence_config.oauth_config.scope ) def test_basic_auth_shared_configuration(self): """Test that basic auth configuration can be shared between services.""" with MockEnvironment.basic_auth_env() as env: # Both services should use consistent authentication jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() assert jira_config.username == confluence_config.username assert jira_config.api_token == confluence_config.api_token assert jira_config.auth_type == confluence_config.auth_type @pytest.mark.anyio async def test_authentication_context_in_request(self): """Test authentication context is properly maintained in request state.""" request = MockFastMCP.create_request() request.state.user_atlassian_auth_type = "oauth" request.state.user_atlassian_token = "test-oauth-token" request.state.user_atlassian_email = "test@example.com" with patch( "mcp_atlassian.servers.dependencies.get_http_request", return_value=request ): # Create mock context with lifespan data ctx = MockFastMCP.create_context() ctx.request_context = MagicMock() ctx.request_context.lifespan_context = { "app_lifespan_context": MainAppContext( full_jira_config=JiraConfig.from_env(), full_confluence_config=ConfluenceConfig.from_env(), read_only=False, enabled_tools=None, ) } # Mock the fetcher creation with ( patch("mcp_atlassian.jira.JiraFetcher") as mock_jira_fetcher, patch( "mcp_atlassian.confluence.ConfluenceFetcher" ) as mock_confluence_fetcher, ): # Mock the current user validation mock_jira_instance = MagicMock() mock_jira_instance.get_current_user_account_id.return_value = "user123" mock_jira_fetcher.return_value = mock_jira_instance mock_confluence_instance = MagicMock() mock_confluence_instance.get_current_user_info.return_value = { "email": "test@example.com", "displayName": "Test User", } mock_confluence_fetcher.return_value = mock_confluence_instance # Get fetchers - should use the same auth context jira_fetcher = await get_jira_fetcher(ctx) confluence_fetcher = await get_confluence_fetcher(ctx) # Verify both fetchers were created with user-specific config assert request.state.jira_fetcher is not None assert request.state.confluence_fetcher is not None @pytest.mark.integration class TestCrossServiceErrorHandling: """Test error handling and propagation across services.""" @pytest.mark.anyio async def test_jira_failure_does_not_affect_confluence(self): """Test that Jira failure doesn't prevent Confluence from working.""" with MockEnvironment.basic_auth_env(): app = AtlassianMCP("Test MCP") # Mock Jira to fail during initialization with patch( "mcp_atlassian.jira.config.JiraConfig.from_env" ) as mock_jira_config: mock_jira_config.side_effect = Exception("Jira config failed") # But Confluence should still work async with main_lifespan(app) as lifespan_data: context = lifespan_data["app_lifespan_context"] assert context.full_jira_config is None assert context.full_confluence_config is not None @pytest.mark.anyio async def test_confluence_failure_does_not_affect_jira(self): """Test that Confluence failure doesn't prevent Jira from working.""" with MockEnvironment.basic_auth_env(): app = AtlassianMCP("Test MCP") # Mock Confluence to fail during initialization with patch( "mcp_atlassian.confluence.config.ConfluenceConfig.from_env" ) as mock_conf_config: mock_conf_config.side_effect = Exception("Confluence config failed") # But Jira should still work async with main_lifespan(app) as lifespan_data: context = lifespan_data["app_lifespan_context"] assert context.full_jira_config is not None assert context.full_confluence_config is None def test_error_propagation_in_user_config_creation(self): """Test error propagation when creating user-specific configurations.""" base_config = JiraConfig.from_env() # Test missing OAuth token with pytest.raises(ValueError, match="OAuth access token missing"): _create_user_config_for_fetcher( base_config=base_config, auth_type="oauth", credentials={"user_email_context": "test@example.com"}, ) # Test missing PAT token with pytest.raises(ValueError, match="PAT missing"): _create_user_config_for_fetcher( base_config=base_config, auth_type="pat", credentials={"user_email_context": "test@example.com"}, ) # Test invalid auth type with pytest.raises(ValueError, match="Unsupported auth_type"): _create_user_config_for_fetcher( base_config=base_config, auth_type="invalid", credentials={} ) @pytest.mark.integration class TestSharedSSLProxyConfiguration: """Test shared SSL and proxy configuration between services.""" def test_ssl_configuration_shared(self): """Test that SSL configuration is applied consistently.""" with MockEnvironment.basic_auth_env(): # Set SSL verification to false for both services with patch.dict( os.environ, {"JIRA_SSL_VERIFY": "false", "CONFLUENCE_SSL_VERIFY": "false"}, ): jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() assert jira_config.ssl_verify is False assert confluence_config.ssl_verify is False # Test SSL adapter configuration jira_session = Session() confluence_session = Session() configure_ssl_verification( "Jira", jira_config.url, jira_session, ssl_verify=False ) configure_ssl_verification( "Confluence", confluence_config.url, confluence_session, ssl_verify=False, ) # Extract domains jira_domain = jira_config.url.split("://")[1].split("/")[0] confluence_domain = confluence_config.url.split("://")[1].split("/")[0] # Both should have SSL ignore adapters assert f"https://{jira_domain}" in jira_session.adapters assert f"https://{confluence_domain}" in confluence_session.adapters def test_proxy_configuration_shared(self): """Test that proxy configuration is shared between services.""" proxy_config = { "HTTP_PROXY": "http://proxy.example.com:8080", "HTTPS_PROXY": "https://proxy.example.com:8443", "NO_PROXY": "localhost,127.0.0.1", } with MockEnvironment.basic_auth_env(): with patch.dict(os.environ, proxy_config): jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() # Both services should have the same proxy configuration assert jira_config.http_proxy == proxy_config["HTTP_PROXY"] assert jira_config.https_proxy == proxy_config["HTTPS_PROXY"] assert jira_config.no_proxy == proxy_config["NO_PROXY"] assert confluence_config.http_proxy == proxy_config["HTTP_PROXY"] assert confluence_config.https_proxy == proxy_config["HTTPS_PROXY"] assert confluence_config.no_proxy == proxy_config["NO_PROXY"] @pytest.mark.integration class TestConcurrentServiceInitialization: """Test concurrent initialization of both services.""" @pytest.mark.anyio async def test_concurrent_service_startup(self): """Test that both services can be initialized concurrently.""" with MockEnvironment.basic_auth_env(): app = AtlassianMCP("Test MCP") # Track initialization order init_order = [] def mock_jira_init(*args, **kwargs): init_order.append("jira_start") # Can't use async sleep in sync function, just append both immediately init_order.append("jira_end") return MagicMock(is_auth_configured=MagicMock(return_value=True)) def mock_confluence_init(*args, **kwargs): init_order.append("confluence_start") init_order.append("confluence_end") return MagicMock(is_auth_configured=MagicMock(return_value=True)) with ( patch( "mcp_atlassian.jira.config.JiraConfig.from_env", side_effect=mock_jira_init, ), patch( "mcp_atlassian.confluence.config.ConfluenceConfig.from_env", side_effect=mock_confluence_init, ), ): async with main_lifespan(app) as lifespan_data: context = lifespan_data["app_lifespan_context"] # Both services should be initialized assert context.full_jira_config is not None assert context.full_confluence_config is not None # Verify concurrent initialization (interleaved order) assert "jira_start" in init_order assert "confluence_start" in init_order @pytest.mark.anyio async def test_parallel_fetcher_creation(self): """Test that fetchers can be created in parallel for both services.""" with MockEnvironment.oauth_env(): # Create mock request with user context request = MockFastMCP.create_request() request.state.user_atlassian_auth_type = "oauth" request.state.user_atlassian_token = "test-token" request.state.user_atlassian_email = "test@example.com" # Create context ctx = MockFastMCP.create_context() ctx.request_context = MagicMock() ctx.request_context.lifespan_context = { "app_lifespan_context": MainAppContext( full_jira_config=JiraConfig.from_env(), full_confluence_config=ConfluenceConfig.from_env(), read_only=False, enabled_tools=None, ) } with ( patch( "mcp_atlassian.servers.dependencies.get_http_request", return_value=request, ), patch("mcp_atlassian.jira.JiraFetcher") as mock_jira_fetcher, patch( "mcp_atlassian.confluence.ConfluenceFetcher" ) as mock_confluence_fetcher, ): # Mock fetcher instances mock_jira_instance = MagicMock() mock_jira_instance.get_current_user_account_id.return_value = "user123" mock_jira_fetcher.return_value = mock_jira_instance mock_confluence_instance = MagicMock() mock_confluence_instance.get_current_user_info.return_value = { "email": "test@example.com", "displayName": "Test User", } mock_confluence_fetcher.return_value = mock_confluence_instance # Create fetchers in parallel using anyio for backend compatibility import anyio async def fetch_jira(): return await get_jira_fetcher(ctx) async def fetch_confluence(): return await get_confluence_fetcher(ctx) # Wait for both using anyio task group async with anyio.create_task_group() as tg: jira_future = None confluence_future = None async def set_jira(): nonlocal jira_future jira_future = await fetch_jira() async def set_confluence(): nonlocal confluence_future confluence_future = await fetch_confluence() tg.start_soon(set_jira) tg.start_soon(set_confluence) jira_fetcher = jira_future confluence_fetcher = confluence_future # Both should be created successfully assert jira_fetcher is not None assert confluence_fetcher is not None assert request.state.jira_fetcher is jira_fetcher assert request.state.confluence_fetcher is confluence_fetcher @pytest.mark.integration class TestServiceAvailabilityDetection: """Test service availability detection and handling.""" def test_detect_no_services_configured(self): """Test detection when no services are configured.""" with MockEnvironment.clean_env(): services = get_available_services() assert services["jira"] is False assert services["confluence"] is False def test_detect_only_jira_configured(self): """Test detection when only Jira is configured.""" with patch.dict( os.environ, { "JIRA_URL": "https://test.atlassian.net", "JIRA_USERNAME": "test@example.com", "JIRA_API_TOKEN": "test-token", }, clear=True, ): # Clear environment to ensure isolation services = get_available_services() assert services["jira"] is True assert services["confluence"] is False def test_detect_only_confluence_configured(self): """Test detection when only Confluence is configured.""" with patch.dict( os.environ, { "CONFLUENCE_URL": "https://test.atlassian.net/wiki", "CONFLUENCE_USERNAME": "test@example.com", "CONFLUENCE_API_TOKEN": "test-token", }, clear=True, ): # Clear environment to ensure isolation services = get_available_services() assert services["jira"] is False assert services["confluence"] is True def test_detect_both_services_configured(self): """Test detection when both services are configured.""" with MockEnvironment.basic_auth_env(): services = get_available_services() assert services["jira"] is True assert services["confluence"] is True def test_partial_configuration_detection(self): """Test detection with partial configuration (URL but no auth).""" with patch.dict( os.environ, { "JIRA_URL": "https://test.atlassian.net", "CONFLUENCE_URL": "https://test.atlassian.net/wiki", # No authentication credentials }, clear=True, ): # Clear environment to ensure isolation services = get_available_services() assert services["jira"] is False assert services["confluence"] is False @pytest.mark.anyio async def test_service_availability_in_lifespan(self): """Test that service availability is properly reflected in lifespan context.""" # Test with only Jira configured with patch.dict( os.environ, { "JIRA_URL": "https://test.atlassian.net", "JIRA_USERNAME": "test@example.com", "JIRA_API_TOKEN": "test-token", }, clear=True, ): app = AtlassianMCP("Test MCP") async with main_lifespan(app) as lifespan_data: context = lifespan_data["app_lifespan_context"] # Only Jira should be configured assert context.full_jira_config is not None assert context.full_confluence_config is None def test_oauth_precedence_over_basic_auth(self): """Test that OAuth configuration takes precedence over basic auth.""" # Set both OAuth and basic auth environment variables with patch.dict( os.environ, { # OAuth configuration "ATLASSIAN_OAUTH_CLIENT_ID": "oauth-client", "ATLASSIAN_OAUTH_CLIENT_SECRET": "oauth-secret", "ATLASSIAN_OAUTH_REDIRECT_URI": "http://localhost:8080", "ATLASSIAN_OAUTH_SCOPE": "read:jira-work write:jira-work", "ATLASSIAN_OAUTH_CLOUD_ID": "cloud-123", # Basic auth configuration "JIRA_URL": "https://test.atlassian.net", "JIRA_USERNAME": "basic@example.com", "JIRA_API_TOKEN": "basic-token", "CONFLUENCE_URL": "https://test.atlassian.net/wiki", "CONFLUENCE_USERNAME": "basic@example.com", "CONFLUENCE_API_TOKEN": "basic-token", }, ): services = get_available_services() assert services["jira"] is True assert services["confluence"] is True # Verify OAuth is used jira_config = JiraConfig.from_env() confluence_config = ConfluenceConfig.from_env() assert jira_config.auth_type == "oauth" assert confluence_config.auth_type == "oauth" assert jira_config.oauth_config is not None assert confluence_config.oauth_config is not None @pytest.mark.integration class TestCrossServiceDataSharing: """Test data sharing and references between services.""" def test_jira_issue_confluence_page_link(self): """Test linking between Jira issues and Confluence pages.""" # Create test data jira_issue = JiraIssueFactory.create( key="TEST-123", fields={ "summary": "Test Issue", "description": "See documentation at https://test.atlassian.net/wiki/spaces/TEST/pages/123456", }, ) confluence_page = ConfluencePageFactory.create( page_id="123456", title="Test Documentation", body={ "storage": { "value": "<p>Related to <a href='https://test.atlassian.net/browse/TEST-123'>TEST-123</a></p>" } }, ) # Verify cross-references exist assert "123456" in jira_issue["fields"]["description"] assert "TEST-123" in confluence_page["body"]["storage"]["value"] def test_shared_user_mentions(self): """Test that user mentions work consistently across services.""" user_account_id = "557058:c4b6b2f1-2f5f-4b85-b033-4cedbe2d2e17" # Jira mention format jira_mention = f"[~accountid:{user_account_id}]" # Confluence mention format confluence_mention = ( f'<ac:link><ri:user ri:account-id="{user_account_id}" /></ac:link>' ) # Create content with mentions jira_comment = {"body": f"Hey {jira_mention}, please review this issue."} confluence_content = { "body": { "storage": { "value": f"<p>Hey {confluence_mention}, please review this page.</p>" } } } # Verify mentions are present assert user_account_id in jira_comment["body"] assert user_account_id in confluence_content["body"]["storage"]["value"]

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ArconixForge/mcp-atlassian'

If you have feedback or need assistance with the MCP directory API, please join our Discord server