Strava MCP Server
by yorrickjansen
Verified
- strava-mcp
- tests
"""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)