Spaces:
Running
Running
File size: 10,055 Bytes
313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a 313d1c4 8f0de8a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
"""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)
|