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,323 @@
"""Tests for booking templates API."""
import pytest
from datetime import datetime, timedelta
from fastapi.testclient import TestClient
def test_create_template(client: TestClient, user_token: str, test_space):
"""Test creating a booking template."""
response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Weekly Team Sync",
"space_id": test_space.id,
"duration_minutes": 60,
"title": "Team Sync Meeting",
"description": "Weekly team synchronization meeting",
},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Weekly Team Sync"
assert data["space_id"] == test_space.id
assert data["space_name"] == test_space.name
assert data["duration_minutes"] == 60
assert data["title"] == "Team Sync Meeting"
assert data["description"] == "Weekly team synchronization meeting"
assert data["usage_count"] == 0
def test_create_template_without_space(client: TestClient, user_token: str):
"""Test creating a template without a default space."""
response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Generic Meeting",
"duration_minutes": 30,
"title": "Meeting",
"description": "Generic meeting template",
},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Generic Meeting"
assert data["space_id"] is None
assert data["space_name"] is None
def test_list_templates(client: TestClient, user_token: str, test_space):
"""Test listing user's templates."""
# Create two templates
client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Template 1",
"space_id": test_space.id,
"duration_minutes": 30,
"title": "Meeting 1",
},
)
client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Template 2",
"space_id": test_space.id,
"duration_minutes": 60,
"title": "Meeting 2",
},
)
# List templates
response = client.get(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
assert any(t["name"] == "Template 1" for t in data)
assert any(t["name"] == "Template 2" for t in data)
def test_list_templates_isolated(client: TestClient, user_token: str, admin_token: str, test_space):
"""Test that users only see their own templates."""
# User creates a template
client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "User Template",
"space_id": test_space.id,
"duration_minutes": 30,
"title": "User Meeting",
},
)
# Admin creates a template
client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Admin Template",
"space_id": test_space.id,
"duration_minutes": 60,
"title": "Admin Meeting",
},
)
# User lists templates
user_response = client.get(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
)
user_data = user_response.json()
# Admin lists templates
admin_response = client.get(
"/api/booking-templates",
headers={"Authorization": f"Bearer {admin_token}"},
)
admin_data = admin_response.json()
# User should only see their template
user_template_names = [t["name"] for t in user_data]
assert "User Template" in user_template_names
assert "Admin Template" not in user_template_names
# Admin should only see their template
admin_template_names = [t["name"] for t in admin_data]
assert "Admin Template" in admin_template_names
assert "User Template" not in admin_template_names
def test_delete_template(client: TestClient, user_token: str, test_space):
"""Test deleting a template."""
# Create template
create_response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "To Delete",
"space_id": test_space.id,
"duration_minutes": 30,
"title": "Meeting",
},
)
template_id = create_response.json()["id"]
# Delete template
delete_response = client.delete(
f"/api/booking-templates/{template_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert delete_response.status_code == 204
# Verify it's gone
list_response = client.get(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
)
templates = list_response.json()
assert not any(t["id"] == template_id for t in templates)
def test_delete_template_not_found(client: TestClient, user_token: str):
"""Test deleting a non-existent template."""
response = client.delete(
"/api/booking-templates/99999",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 404
def test_delete_template_other_user(client: TestClient, user_token: str, admin_token: str, test_space):
"""Test that users cannot delete other users' templates."""
# Admin creates a template
create_response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"name": "Admin Template",
"space_id": test_space.id,
"duration_minutes": 30,
"title": "Meeting",
},
)
template_id = create_response.json()["id"]
# User tries to delete admin's template
delete_response = client.delete(
f"/api/booking-templates/{template_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert delete_response.status_code == 404
def test_create_booking_from_template(client: TestClient, user_token: str, test_space):
"""Test creating a booking from a template."""
# Create template
template_response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Client Meeting",
"space_id": test_space.id,
"duration_minutes": 90,
"title": "Client Presentation",
"description": "Quarterly review with client",
},
)
template_id = template_response.json()["id"]
# Create booking from template
tomorrow = datetime.now() + timedelta(days=1)
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
response = client.post(
f"/api/booking-templates/from-template/{template_id}",
headers={"Authorization": f"Bearer {user_token}"},
params={"start_datetime": start_time.isoformat()},
)
assert response.status_code == 201
data = response.json()
assert data["space_id"] == test_space.id
assert data["title"] == "Client Presentation"
assert data["description"] == "Quarterly review with client"
assert data["status"] == "pending"
# Verify duration
start_dt = datetime.fromisoformat(data["start_datetime"])
end_dt = datetime.fromisoformat(data["end_datetime"])
duration = (end_dt - start_dt).total_seconds() / 60
assert duration == 90
# Verify usage count incremented
list_response = client.get(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
)
templates = list_response.json()
template = next(t for t in templates if t["id"] == template_id)
assert template["usage_count"] == 1
def test_create_booking_from_template_no_space(client: TestClient, user_token: str):
"""Test creating a booking from a template without a default space."""
# Create template without space
template_response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Generic Meeting",
"duration_minutes": 60,
"title": "Meeting",
},
)
template_id = template_response.json()["id"]
# Try to create booking
tomorrow = datetime.now() + timedelta(days=1)
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
response = client.post(
f"/api/booking-templates/from-template/{template_id}",
headers={"Authorization": f"Bearer {user_token}"},
params={"start_datetime": start_time.isoformat()},
)
assert response.status_code == 400
assert "does not have a default space" in response.json()["detail"]
def test_create_booking_from_template_not_found(client: TestClient, user_token: str):
"""Test creating a booking from a non-existent template."""
tomorrow = datetime.now() + timedelta(days=1)
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
response = client.post(
"/api/booking-templates/from-template/99999",
headers={"Authorization": f"Bearer {user_token}"},
params={"start_datetime": start_time.isoformat()},
)
assert response.status_code == 404
def test_create_booking_from_template_validation_error(client: TestClient, user_token: str, test_space):
"""Test that booking from template validates booking rules."""
# Create template
template_response = client.post(
"/api/booking-templates",
headers={"Authorization": f"Bearer {user_token}"},
json={
"name": "Long Meeting",
"space_id": test_space.id,
"duration_minutes": 600, # 10 hours - exceeds max
"title": "Marathon Meeting",
},
)
template_id = template_response.json()["id"]
# Try to create booking
tomorrow = datetime.now() + timedelta(days=1)
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
response = client.post(
f"/api/booking-templates/from-template/{template_id}",
headers={"Authorization": f"Bearer {user_token}"},
params={"start_datetime": start_time.isoformat()},
)
assert response.status_code == 400
# Should fail validation (duration exceeds max)