"""Tests for Google OAuth service. Tests the Google OAuth flow with mocked HTTP responses using respx. """ from unittest.mock import patch import pytest import respx from httpx import Response from app.services.oauth.google import GoogleOAuth, GoogleOAuthError pytestmark = pytest.mark.asyncio class TestGetAuthorizationUrl: """Tests for get_authorization_url method.""" def test_raises_when_not_configured(self): """Test that get_authorization_url raises when Google OAuth is not configured. Without client ID, the method should raise GoogleOAuthError. """ oauth = GoogleOAuth() with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = None with pytest.raises(GoogleOAuthError, match="not configured"): oauth.get_authorization_url("http://localhost/callback", "state123") def test_returns_valid_url_when_configured(self): """Test that get_authorization_url returns properly formatted URL. The URL should include client_id, redirect_uri, state, and scopes. """ oauth = GoogleOAuth() with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret = "test-secret" url = oauth.get_authorization_url("http://localhost/callback", "state123") assert "accounts.google.com/o/oauth2/v2/auth" in url assert "client_id=test-client-id" in url assert "redirect_uri=http" in url assert "state=state123" in url assert "scope=" in url assert "response_type=code" in url class TestExchangeCodeForTokens: """Tests for exchange_code_for_tokens method.""" @respx.mock async def test_returns_tokens_on_success(self): """Test that exchange_code_for_tokens returns tokens on success. Mocks Google's token endpoint to return valid tokens. """ oauth = GoogleOAuth() respx.post("https://oauth2.googleapis.com/token").mock( return_value=Response( 200, json={ "access_token": "test-access-token", "id_token": "test-id-token", "expires_in": 3600, "token_type": "Bearer", }, ) ) with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret.get_secret_value.return_value = "test-secret" tokens = await oauth.exchange_code_for_tokens("auth-code", "http://localhost/callback") assert tokens["access_token"] == "test-access-token" @respx.mock async def test_raises_on_error_response(self): """Test that exchange_code_for_tokens raises on error from Google. If Google returns an error, GoogleOAuthError should be raised. """ oauth = GoogleOAuth() respx.post("https://oauth2.googleapis.com/token").mock( return_value=Response( 400, json={ "error": "invalid_grant", "error_description": "Code has expired", }, ) ) with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret.get_secret_value.return_value = "test-secret" with pytest.raises(GoogleOAuthError, match="Token exchange failed"): await oauth.exchange_code_for_tokens("expired-code", "http://localhost/callback") class TestFetchUserInfo: """Tests for fetch_user_info method.""" @respx.mock async def test_returns_user_info_on_success(self): """Test that fetch_user_info returns user data from Google. Mocks Google's userinfo endpoint. """ oauth = GoogleOAuth() respx.get("https://www.googleapis.com/oauth2/v2/userinfo").mock( return_value=Response( 200, json={ "id": "google-user-123", "email": "user@gmail.com", "name": "Test User", "picture": "https://google.com/avatar.jpg", }, ) ) user_info = await oauth.fetch_user_info("test-access-token") assert user_info["id"] == "google-user-123" assert user_info["email"] == "user@gmail.com" @respx.mock async def test_raises_on_error_response(self): """Test that fetch_user_info raises on error from Google.""" oauth = GoogleOAuth() respx.get("https://www.googleapis.com/oauth2/v2/userinfo").mock( return_value=Response(401, json={"error": "Invalid token"}) ) with pytest.raises(GoogleOAuthError, match="Failed to fetch user info"): await oauth.fetch_user_info("invalid-token") class TestGetUserInfo: """Tests for get_user_info method (full flow).""" @respx.mock async def test_returns_oauth_user_info_on_success(self): """Test that get_user_info completes full OAuth flow. This tests the combined token exchange + user info fetch. """ oauth = GoogleOAuth() # Mock token exchange respx.post("https://oauth2.googleapis.com/token").mock( return_value=Response( 200, json={ "access_token": "test-access-token", "id_token": "test-id-token", "expires_in": 3600, }, ) ) # Mock user info respx.get("https://www.googleapis.com/oauth2/v2/userinfo").mock( return_value=Response( 200, json={ "id": "google-user-456", "email": "fullflow@gmail.com", "name": "Full Flow User", "picture": "https://google.com/fullflow.jpg", }, ) ) with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret.get_secret_value.return_value = "test-secret" result = await oauth.get_user_info("auth-code", "http://localhost/callback") assert result.provider == "google" assert result.oauth_id == "google-user-456" assert result.email == "fullflow@gmail.com" assert result.name == "Full Flow User" assert result.avatar_url == "https://google.com/fullflow.jpg" @respx.mock async def test_raises_when_no_access_token(self): """Test that get_user_info raises when token response lacks access_token.""" oauth = GoogleOAuth() respx.post("https://oauth2.googleapis.com/token").mock( return_value=Response( 200, json={"id_token": "only-id-token"}, # No access_token ) ) with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret.get_secret_value.return_value = "test-secret" with pytest.raises(GoogleOAuthError, match="No access token"): await oauth.get_user_info("auth-code", "http://localhost/callback") class TestIsConfigured: """Tests for is_configured method.""" def test_returns_false_when_not_configured(self): """Test that is_configured returns False without credentials.""" oauth = GoogleOAuth() with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = None mock_settings.google_client_secret = None assert oauth.is_configured() is False def test_returns_true_when_configured(self): """Test that is_configured returns True with credentials.""" oauth = GoogleOAuth() with patch("app.services.oauth.google.settings") as mock_settings: mock_settings.google_client_id = "client-id" mock_settings.google_client_secret = "client-secret" assert oauth.is_configured() is True