Files
space-booking/backend/tests/test_google_calendar.py
Claude Agent df4031d99c feat: Space Booking System - MVP complet
Sistem web pentru rezervarea de birouri și săli de ședință
cu flux de aprobare administrativă.

Stack: FastAPI + Vue.js 3 + SQLite + TypeScript

Features implementate:
- Autentificare JWT + Self-registration cu email verification
- CRUD Spații, Utilizatori, Settings (Admin)
- Calendar interactiv (FullCalendar) cu drag-and-drop
- Creare rezervări cu validare (durată, program, overlap, max/zi)
- Rezervări recurente (săptămânal)
- Admin: aprobare/respingere/anulare cereri
- Admin: creare directă rezervări (bypass approval)
- Admin: editare orice rezervare
- User: editare/anulare rezervări proprii
- Notificări in-app (bell icon + dropdown)
- Notificări email (async SMTP cu BackgroundTasks)
- Jurnal acțiuni administrative (audit log)
- Rapoarte avansate (utilizare, top users, approval rate)
- Șabloane rezervări (booking templates)
- Atașamente fișiere (upload/download)
- Conflict warnings (verificare disponibilitate real-time)
- Integrare Google Calendar (OAuth2)
- Suport timezone (UTC storage + user preference)
- 225+ teste backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 17:51:29 +00:00

411 lines
13 KiB
Python

"""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
)