""" Unit tests for Google Calendar OAuth endpoints. Tests: - /install endpoint returns OAuth URL with HMAC state - /callback verifies state, stores encrypted token in DB - /status returns connected=True when token exists, False otherwise - HMAC state generation and verification work correctly - Missing credentials configuration handled gracefully """ from __future__ import annotations import base64 import json import uuid from unittest.mock import AsyncMock, MagicMock, patch import pytest _SECRET = "test-oauth-state-secret" _TENANT_ID = str(uuid.uuid4()) # --------------------------------------------------------------------------- # HMAC state helper tests (reuse from channels.py) # --------------------------------------------------------------------------- def test_calendar_install_builds_oauth_url(): """ calendar_install endpoint returns a dict with a 'url' key pointing at accounts.google.com/o/oauth2/v2/auth. """ from shared.api.calendar_auth import build_calendar_oauth_url url = build_calendar_oauth_url(tenant_id=_TENANT_ID, secret=_SECRET) assert "accounts.google.com/o/oauth2/v2/auth" in url assert "client_id=" in url assert "scope=" in url assert "state=" in url assert "access_type=offline" in url assert "prompt=consent" in url def test_calendar_oauth_url_contains_signed_state(): """State parameter in the OAuth URL encodes the tenant_id.""" from shared.api.calendar_auth import build_calendar_oauth_url from shared.api.channels import verify_oauth_state url = build_calendar_oauth_url(tenant_id=_TENANT_ID, secret=_SECRET) # Extract state from URL import urllib.parse parsed = urllib.parse.urlparse(url) params = urllib.parse.parse_qs(parsed.query) state = params["state"][0] # Verify the state recovers the tenant_id recovered = verify_oauth_state(state=state, secret=_SECRET) assert recovered == _TENANT_ID def test_calendar_oauth_url_uses_calendar_scope(): """OAuth URL requests full Google Calendar scope.""" from shared.api.calendar_auth import build_calendar_oauth_url url = build_calendar_oauth_url(tenant_id=_TENANT_ID, secret=_SECRET) assert "googleapis.com/auth/calendar" in url # --------------------------------------------------------------------------- # Callback token exchange and storage # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_calendar_callback_stores_encrypted_token(): """ handle_calendar_callback() exchanges code for tokens, encrypts them, and upserts a ChannelConnection with channel_type='google_calendar'. """ from shared.api.calendar_auth import handle_calendar_callback mock_session = AsyncMock() mock_session.execute = AsyncMock() mock_session.add = MagicMock() mock_session.commit = AsyncMock() # Simulate no existing connection (first install) mock_result = MagicMock() mock_result.scalar_one_or_none.return_value = None mock_session.execute.return_value = mock_result token_response = { "access_token": "ya29.test_access_token", "refresh_token": "1//test_refresh_token", "token_type": "Bearer", "expires_in": 3600, } with ( patch("shared.api.calendar_auth.httpx.AsyncClient") as mock_client_cls, patch("shared.api.calendar_auth.KeyEncryptionService") as mock_enc_cls, patch("shared.api.calendar_auth.settings") as mock_settings, ): mock_settings.oauth_state_secret = _SECRET mock_settings.google_client_id = "test-client-id" mock_settings.google_client_secret = "test-client-secret" mock_settings.portal_url = "http://localhost:3000" mock_settings.platform_encryption_key = "test-key" mock_settings.platform_encryption_key_previous = "" mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = token_response mock_http = AsyncMock() mock_http.__aenter__ = AsyncMock(return_value=mock_http) mock_http.__aexit__ = AsyncMock(return_value=None) mock_http.post = AsyncMock(return_value=mock_response) mock_client_cls.return_value = mock_http mock_enc = MagicMock() mock_enc.encrypt.return_value = "encrypted_token_data" mock_enc_cls.return_value = mock_enc # Generate a valid state from shared.api.channels import generate_oauth_state state = generate_oauth_state(tenant_id=_TENANT_ID, secret=_SECRET) redirect_url = await handle_calendar_callback( code="test_auth_code", state=state, session=mock_session, ) # Should redirect to portal settings assert "settings" in redirect_url or "calendar" in redirect_url # Session.add should have been called (new ChannelConnection) mock_session.add.assert_called_once() # Encryption was called mock_enc.encrypt.assert_called_once() # The ChannelConnection passed to add should have google_calendar type conn = mock_session.add.call_args[0][0] assert "google_calendar" in str(conn.channel_type).lower() @pytest.mark.asyncio async def test_calendar_callback_invalid_state_raises(): """handle_calendar_callback raises HTTPException for tampered state.""" from fastapi import HTTPException from shared.api.calendar_auth import handle_calendar_callback mock_session = AsyncMock() with patch("shared.api.calendar_auth.settings") as mock_settings: mock_settings.oauth_state_secret = _SECRET with pytest.raises(HTTPException) as exc_info: await handle_calendar_callback( code="some_code", state="TAMPERED.INVALID", session=mock_session, ) assert exc_info.value.status_code == 400 # --------------------------------------------------------------------------- # Status endpoint # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_calendar_status_connected(): """get_calendar_status returns connected=True when ChannelConnection exists.""" from shared.api.calendar_auth import get_calendar_status mock_session = AsyncMock() mock_result = MagicMock() # Simulate existing connection mock_conn = MagicMock() mock_result.scalar_one_or_none.return_value = mock_conn mock_session.execute.return_value = mock_result tenant_id = uuid.uuid4() status = await get_calendar_status(tenant_id=tenant_id, session=mock_session) assert status["connected"] is True @pytest.mark.asyncio async def test_calendar_status_not_connected(): """get_calendar_status returns connected=False when no ChannelConnection exists.""" from shared.api.calendar_auth import get_calendar_status mock_session = AsyncMock() mock_result = MagicMock() mock_result.scalar_one_or_none.return_value = None mock_session.execute.return_value = mock_result tenant_id = uuid.uuid4() status = await get_calendar_status(tenant_id=tenant_id, session=mock_session) assert status["connected"] is False