"""Tests for Google Calendar integration.""" from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models.booking import Booking from app.models.google_calendar_token import GoogleCalendarToken from app.models.space import Space from app.models.user import User from app.services.google_calendar_service import ( create_calendar_event, delete_calendar_event, get_google_calendar_service, ) class TestGoogleCalendarAPI: """Test Google Calendar API endpoints.""" def test_status_not_connected( self, client: TestClient, test_user: User, auth_headers: dict ): """Test status endpoint when not connected.""" response = client.get("/api/integrations/google/status", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["connected"] is False assert data["expires_at"] is None def test_connect_missing_credentials( self, client: TestClient, test_user: User, auth_headers: dict ): """Test connect endpoint with missing Google credentials.""" # Note: In conftest, google_client_id and google_client_secret are empty by default response = client.get("/api/integrations/google/connect", headers=auth_headers) assert response.status_code == 503 assert "not configured" in response.json()["detail"].lower() @patch("app.api.google_calendar.Flow") def test_connect_success( self, mock_flow, client: TestClient, test_user: User, auth_headers: dict ): """Test successful OAuth flow initiation.""" # Mock the Flow object mock_flow_instance = MagicMock() mock_flow_instance.authorization_url.return_value = ( "https://accounts.google.com/o/oauth2/auth?...", "test_state", ) mock_flow.from_client_config.return_value = mock_flow_instance # Temporarily set credentials in settings from app.core.config import settings original_client_id = settings.google_client_id original_client_secret = settings.google_client_secret settings.google_client_id = "test_client_id" settings.google_client_secret = "test_client_secret" try: response = client.get( "/api/integrations/google/connect", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert "authorization_url" in data assert "state" in data assert data["state"] == "test_state" finally: # Restore original settings settings.google_client_id = original_client_id settings.google_client_secret = original_client_secret def test_disconnect_no_token( self, client: TestClient, test_user: User, auth_headers: dict ): """Test disconnect when no token exists.""" response = client.delete( "/api/integrations/google/disconnect", headers=auth_headers ) assert response.status_code == 200 assert "disconnected" in response.json()["message"].lower() def test_disconnect_with_token( self, client: TestClient, test_user: User, auth_headers: dict, db: Session ): """Test disconnect when token exists.""" # Create a token for the user token = GoogleCalendarToken( user_id=test_user.id, access_token="test_access_token", refresh_token="test_refresh_token", token_expiry=None, ) db.add(token) db.commit() response = client.delete( "/api/integrations/google/disconnect", headers=auth_headers ) assert response.status_code == 200 assert "disconnected" in response.json()["message"].lower() # Verify token was deleted deleted_token = ( db.query(GoogleCalendarToken) .filter(GoogleCalendarToken.user_id == test_user.id) .first() ) assert deleted_token is None def test_status_connected( self, client: TestClient, test_user: User, auth_headers: dict, db: Session ): """Test status endpoint when connected.""" from datetime import datetime, timedelta expiry = datetime.utcnow() + timedelta(hours=1) # Create a token for the user token = GoogleCalendarToken( user_id=test_user.id, access_token="test_access_token", refresh_token="test_refresh_token", token_expiry=expiry, ) db.add(token) db.commit() response = client.get("/api/integrations/google/status", headers=auth_headers) assert response.status_code == 200 data = response.json() assert data["connected"] is True assert data["expires_at"] is not None class TestGoogleCalendarService: """Test Google Calendar service functions.""" def test_get_service_no_token(self, db: Session, test_user: User): """Test getting service when no token exists.""" service = get_google_calendar_service(db, test_user.id) # type: ignore[arg-type] assert service is None @patch("app.services.google_calendar_service.build") @patch("app.services.google_calendar_service.Credentials") def test_create_calendar_event_success( self, mock_credentials, mock_build, db: Session, test_user: User ): """Test successful calendar event creation.""" from datetime import datetime, timedelta # Create token token = GoogleCalendarToken( user_id=test_user.id, access_token="test_access_token", refresh_token="test_refresh_token", ) db.add(token) # Create space space = Space( name="Test Conference Room", type="sala", description="A test room", capacity=10, ) db.add(space) # Create booking now = datetime.utcnow() booking = Booking( user_id=test_user.id, space_id=1, title="Test Meeting", description="Test description", start_datetime=now, end_datetime=now + timedelta(hours=1), status="approved", ) db.add(booking) db.commit() db.refresh(booking) # Mock Google API mock_service = MagicMock() mock_service.events().insert().execute.return_value = {"id": "google_event_123"} mock_build.return_value = mock_service # Mock credentials mock_creds_instance = MagicMock() mock_creds_instance.expired = False mock_creds_instance.refresh_token = "test_refresh_token" mock_credentials.return_value = mock_creds_instance # Create event event_id = create_calendar_event(db, booking, test_user.id) # type: ignore[arg-type] assert event_id == "google_event_123" # Check that insert was called (not assert_called_once due to mock chaining) assert mock_service.events().insert.call_count >= 1 @patch("app.services.google_calendar_service.build") @patch("app.services.google_calendar_service.Credentials") def test_delete_calendar_event_success( self, mock_credentials, mock_build, db: Session, test_user: User ): """Test successful calendar event deletion.""" # Create token token = GoogleCalendarToken( user_id=test_user.id, access_token="test_access_token", refresh_token="test_refresh_token", ) db.add(token) db.commit() # Mock Google API mock_service = MagicMock() mock_build.return_value = mock_service # Mock credentials mock_creds_instance = MagicMock() mock_creds_instance.expired = False mock_creds_instance.refresh_token = "test_refresh_token" mock_credentials.return_value = mock_creds_instance # Delete event result = delete_calendar_event(db, "google_event_123", test_user.id) # type: ignore[arg-type] assert result is True mock_service.events().delete.assert_called_once_with( calendarId="primary", eventId="google_event_123" ) def test_create_event_no_token(self, db: Session, test_user: User): """Test creating event when user has no token.""" from datetime import datetime, timedelta # Create space and booking without token space = Space( name="Test Room", type="sala", description="Test", capacity=10, ) db.add(space) now = datetime.utcnow() booking = Booking( user_id=test_user.id, space_id=1, title="Test", start_datetime=now, end_datetime=now + timedelta(hours=1), status="approved", ) db.add(booking) db.commit() db.refresh(booking) event_id = create_calendar_event(db, booking, test_user.id) # type: ignore[arg-type] assert event_id is None class TestBookingGoogleCalendarIntegration: """Test integration of Google Calendar with booking approval/cancellation.""" @patch("app.services.google_calendar_service.create_calendar_event") def test_booking_approval_creates_event( self, mock_create_event, client: TestClient, test_admin: User, admin_headers: dict, db: Session, ): """Test that approving a booking creates a Google Calendar event.""" from datetime import datetime, timedelta # Create test user and token user = User( email="user@test.com", full_name="Test User", hashed_password="hashed", role="user", ) db.add(user) db.commit() db.refresh(user) token = GoogleCalendarToken( user_id=user.id, access_token="test_access_token", refresh_token="test_refresh_token", ) db.add(token) # Create space space = Space( name="Test Room", type="sala", description="Test", capacity=10, ) db.add(space) db.commit() db.refresh(space) # Create pending booking now = datetime.utcnow() booking = Booking( user_id=user.id, space_id=space.id, title="Test Meeting", start_datetime=now + timedelta(hours=2), end_datetime=now + timedelta(hours=3), status="pending", ) db.add(booking) db.commit() db.refresh(booking) # Mock Google Calendar event creation mock_create_event.return_value = "google_event_123" # Approve booking response = client.put( f"/api/admin/bookings/{booking.id}/approve", headers=admin_headers ) assert response.status_code == 200 data = response.json() assert data["status"] == "approved" assert data["google_calendar_event_id"] == "google_event_123" # Verify event creation was called mock_create_event.assert_called_once() @patch("app.services.google_calendar_service.delete_calendar_event") def test_booking_cancellation_deletes_event( self, mock_delete_event, client: TestClient, test_user: User, auth_headers: dict, db: Session, ): """Test that canceling a booking deletes the Google Calendar event.""" from datetime import datetime, timedelta # Create token token = GoogleCalendarToken( user_id=test_user.id, access_token="test_access_token", refresh_token="test_refresh_token", ) db.add(token) # Create space space = Space( name="Test Room", type="sala", description="Test", capacity=10, ) db.add(space) db.commit() db.refresh(space) # Create approved booking with Google Calendar event now = datetime.utcnow() booking = Booking( user_id=test_user.id, space_id=space.id, title="Test Meeting", start_datetime=now + timedelta(hours=3), end_datetime=now + timedelta(hours=4), status="approved", google_calendar_event_id="google_event_123", ) db.add(booking) db.commit() # Mock Google Calendar event deletion mock_delete_event.return_value = True # Cancel booking response = client.put( f"/api/bookings/{booking.id}/cancel", headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["status"] == "canceled" assert data["google_calendar_event_id"] is None # Verify event deletion was called mock_delete_event.assert_called_once_with( db=db, event_id="google_event_123", user_id=test_user.id )