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:
Claude Agent
2026-02-09 17:51:29 +00:00
commit df4031d99c
113 changed files with 24491 additions and 0 deletions

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