Strava MCP Server

by yorrickjansen
Verified
"""Tests for the Strava authentication module.""" import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI from httpx import Response from strava_mcp.auth import StravaAuthenticator, get_strava_refresh_token @pytest.fixture def client_credentials(): """Fixture for client credentials.""" return { "client_id": "test_client_id", "client_secret": "test_client_secret", } @pytest.fixture def mock_token_response(): """Fixture for token response.""" return { "access_token": "test_access_token", "refresh_token": "test_refresh_token", "expires_at": 1609459200, "expires_in": 21600, "token_type": "Bearer", } @pytest.fixture def fastapi_app(): """Fixture for FastAPI app.""" return FastAPI() @pytest.fixture def authenticator(client_credentials, fastapi_app): """Fixture for StravaAuthenticator.""" return StravaAuthenticator( client_id=client_credentials["client_id"], client_secret=client_credentials["client_secret"], app=fastapi_app, ) def test_get_authorization_url(authenticator): """Test getting the authorization URL.""" url = authenticator.get_authorization_url() # Check that the URL contains the expected parameters assert "https://www.strava.com/oauth/authorize" in url assert f"client_id={authenticator.client_id}" in url # URL is encoded, so we need to check the non-encoded parts assert "redirect_uri=http%3A%2F%2F127.0.0.1%3A3008%2Fexchange_token" in url assert "response_type=code" in url assert "scope=" in url def test_setup_routes(authenticator, fastapi_app): """Test setting up routes.""" authenticator.setup_routes(fastapi_app) # Check that the routes were added routes = [route.path for route in fastapi_app.routes] assert authenticator.redirect_path in routes assert "/auth" in routes def test_setup_routes_no_app(authenticator): """Test setting up routes with no app.""" authenticator.app = None with pytest.raises(ValueError, match="No FastAPI app provided"): authenticator.setup_routes() @pytest.mark.asyncio async def test_exchange_token_success(authenticator, mock_token_response): """Test exchanging token successfully.""" # Setup mock with patch("httpx.AsyncClient") as mock_client: mock_response = MagicMock(spec=Response) mock_response.status_code = 200 mock_response.json.return_value = mock_token_response mock_client.return_value.__aenter__.return_value.post.return_value = mock_response # Set up a future to receive the token authenticator.token_future = asyncio.Future() # Call the handler response = await authenticator.exchange_token(code="test_code") # Check response assert response.status_code == 200 assert "Authorization successful" in response.body.decode() # Check token future assert authenticator.token_future.done() assert await authenticator.token_future == "test_refresh_token" # Check token was saved assert authenticator.refresh_token == "test_refresh_token" # Verify correct API call mock_client.return_value.__aenter__.return_value.post.assert_called_once() args, kwargs = mock_client.return_value.__aenter__.return_value.post.call_args assert args[0] == "https://www.strava.com/oauth/token" assert kwargs["data"]["client_id"] == authenticator.client_id assert kwargs["data"]["client_secret"] == authenticator.client_secret assert kwargs["data"]["code"] == "test_code" assert kwargs["data"]["grant_type"] == "authorization_code" @pytest.mark.asyncio async def test_exchange_token_failure(authenticator): """Test exchanging token with failure.""" # Setup mock with patch("httpx.AsyncClient") as mock_client: mock_response = MagicMock(spec=Response) mock_response.status_code = 400 mock_response.text = "Invalid code" mock_client.return_value.__aenter__.return_value.post.return_value = mock_response # Set up a future to receive the token authenticator.token_future = asyncio.Future() # Call the handler response = await authenticator.exchange_token(code="invalid_code") # Check response assert response.status_code == 200 assert "Authorization failed" in response.body.decode() # Check token future assert authenticator.token_future.done() # We expect a specific exception here, so using pytest.raises is appropriate with pytest.raises(Exception): # noqa: B017 await authenticator.token_future @pytest.mark.asyncio async def test_start_auth_flow(authenticator): """Test starting auth flow.""" with patch.object(authenticator, "get_authorization_url", return_value="https://example.com/auth"): response = await authenticator.start_auth_flow() assert response.status_code == 307 assert response.headers["location"] == "https://example.com/auth" @pytest.mark.asyncio async def test_get_refresh_token(authenticator): """Test getting refresh token.""" # Mock the webbrowser.open call with patch("webbrowser.open", return_value=True) as mock_open: with patch.object(authenticator, "get_authorization_url", return_value="https://example.com/auth"): # Set the future result after a delay authenticator.token_future = None # Reset it so a new one is created # Start the token request in background task = asyncio.create_task(authenticator.get_refresh_token()) # Wait a bit and set the result await asyncio.sleep(0.1) # Initialize the token_future before setting result if not authenticator.token_future: authenticator.token_future = asyncio.Future() authenticator.token_future.set_result("test_refresh_token") # Get the result token = await task # Verify assert token == "test_refresh_token" mock_open.assert_called_once_with("https://example.com/auth") @pytest.mark.asyncio async def test_get_refresh_token_no_browser(authenticator): """Test getting refresh token without opening browser.""" with patch("webbrowser.open") as mock_open: with patch.object(authenticator, "get_authorization_url", return_value="https://example.com/auth"): # Set the future result after a delay authenticator.token_future = None # Reset it so a new one is created # Start the token request in background task = asyncio.create_task(authenticator.get_refresh_token(open_browser=False)) # Wait a bit and set the result await asyncio.sleep(0.1) # Initialize the token_future before setting result if not authenticator.token_future: authenticator.token_future = asyncio.Future() authenticator.token_future.set_result("test_refresh_token") # Get the result token = await task # Verify assert token == "test_refresh_token" mock_open.assert_not_called() @pytest.mark.asyncio async def test_get_refresh_token_browser_fails(authenticator): """Test getting refresh token with browser opening failing.""" with patch("webbrowser.open", return_value=False) as mock_open: with patch.object(authenticator, "get_authorization_url", return_value="https://example.com/auth"): # Set the future result after a delay authenticator.token_future = None # Reset it so a new one is created # Start the token request in background task = asyncio.create_task(authenticator.get_refresh_token()) # Wait a bit and set the result await asyncio.sleep(0.1) # Initialize the token_future before setting result if not authenticator.token_future: authenticator.token_future = asyncio.Future() authenticator.token_future.set_result("test_refresh_token") # Get the result token = await task # Verify assert token == "test_refresh_token" mock_open.assert_called_once_with("https://example.com/auth") @pytest.mark.asyncio async def test_get_strava_refresh_token(client_credentials): """Test get_strava_refresh_token function.""" with patch("strava_mcp.auth.StravaAuthenticator") as mock_authenticator_class: # Setup mock mock_authenticator = MagicMock() mock_authenticator.get_refresh_token = AsyncMock(return_value="test_refresh_token") mock_authenticator.setup_routes = MagicMock() mock_authenticator_class.return_value = mock_authenticator # Test without app token = await get_strava_refresh_token(client_credentials["client_id"], client_credentials["client_secret"]) # Verify assert token == "test_refresh_token" mock_authenticator_class.assert_called_once_with( client_credentials["client_id"], client_credentials["client_secret"], None ) mock_authenticator.setup_routes.assert_not_called() # Reset mocks mock_authenticator_class.reset_mock() mock_authenticator.get_refresh_token.reset_mock() mock_authenticator.setup_routes.reset_mock() # Test with app app = FastAPI() token = await get_strava_refresh_token( client_credentials["client_id"], client_credentials["client_secret"], app ) # Verify assert token == "test_refresh_token" mock_authenticator_class.assert_called_once_with( client_credentials["client_id"], client_credentials["client_secret"], app ) mock_authenticator.setup_routes.assert_called_once_with(app)