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>
This commit is contained in:
410
backend/tests/test_google_calendar.py
Normal file
410
backend/tests/test_google_calendar.py
Normal file
@@ -0,0 +1,410 @@
|
||||
"""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
|
||||
)
|
||||
Reference in New Issue
Block a user