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,328 @@
"""Tests for space management endpoints."""
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.user import User
def test_create_space_as_admin(client: TestClient, admin_token: str) -> None:
"""Test creating a space as admin."""
response = client.post(
"/api/admin/spaces",
json={
"name": "Conference Room A",
"type": "sala",
"capacity": 10,
"description": "Main conference room",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 201
data = response.json()
assert data["name"] == "Conference Room A"
assert data["type"] == "sala"
assert data["capacity"] == 10
assert data["is_active"] is True
def test_create_space_as_user_forbidden(client: TestClient, user_token: str) -> None:
"""Test that regular users cannot create spaces."""
response = client.post(
"/api/admin/spaces",
json={
"name": "Test Space",
"type": "birou",
"capacity": 1,
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 403
def test_create_space_duplicate_name(client: TestClient, admin_token: str) -> None:
"""Test that duplicate space names are rejected."""
space_data = {
"name": "Duplicate Room",
"type": "sala",
"capacity": 5,
}
# Create first space
response = client.post(
"/api/admin/spaces",
json=space_data,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 201
# Try to create duplicate
response = client.post(
"/api/admin/spaces",
json=space_data,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 400
assert "already exists" in response.json()["detail"]
def test_create_space_invalid_capacity(client: TestClient, admin_token: str) -> None:
"""Test that invalid capacity is rejected."""
response = client.post(
"/api/admin/spaces",
json={
"name": "Invalid Space",
"type": "sala",
"capacity": 0,
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 422 # Validation error
def test_create_space_invalid_type(client: TestClient, admin_token: str) -> None:
"""Test that invalid type is rejected."""
response = client.post(
"/api/admin/spaces",
json={
"name": "Invalid Type Space",
"type": "invalid",
"capacity": 5,
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 422 # Validation error
def test_list_spaces_as_user(client: TestClient, user_token: str, admin_token: str) -> None:
"""Test that users see only active spaces."""
# Create active space
client.post(
"/api/admin/spaces",
json={"name": "Active Space", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
# Create inactive space
response = client.post(
"/api/admin/spaces",
json={"name": "Inactive Space", "type": "birou", "capacity": 1},
headers={"Authorization": f"Bearer {admin_token}"},
)
space_id = response.json()["id"]
# Deactivate the second space
client.patch(
f"/api/admin/spaces/{space_id}/status",
json={"is_active": False},
headers={"Authorization": f"Bearer {admin_token}"},
)
# List as user - should see only active
response = client.get(
"/api/spaces",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == 200
spaces = response.json()
names = [s["name"] for s in spaces]
assert "Active Space" in names
assert "Inactive Space" not in names
def test_list_spaces_as_admin(client: TestClient, admin_token: str) -> None:
"""Test that admins see all spaces."""
# Create active space
client.post(
"/api/admin/spaces",
json={"name": "Admin View Active", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
# Create inactive space
response = client.post(
"/api/admin/spaces",
json={"name": "Admin View Inactive", "type": "birou", "capacity": 1},
headers={"Authorization": f"Bearer {admin_token}"},
)
space_id = response.json()["id"]
# Deactivate
client.patch(
f"/api/admin/spaces/{space_id}/status",
json={"is_active": False},
headers={"Authorization": f"Bearer {admin_token}"},
)
# List as admin - should see both
response = client.get(
"/api/spaces",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
spaces = response.json()
names = [s["name"] for s in spaces]
assert "Admin View Active" in names
assert "Admin View Inactive" in names
def test_update_space(client: TestClient, admin_token: str) -> None:
"""Test updating a space."""
# Create space
response = client.post(
"/api/admin/spaces",
json={"name": "Original Name", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
space_id = response.json()["id"]
# Update space
response = client.put(
f"/api/admin/spaces/{space_id}",
json={
"name": "Updated Name",
"type": "birou",
"capacity": 2,
"description": "Updated description",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
assert data["type"] == "birou"
assert data["capacity"] == 2
assert data["description"] == "Updated description"
def test_update_space_not_found(client: TestClient, admin_token: str) -> None:
"""Test updating non-existent space."""
response = client.put(
"/api/admin/spaces/99999",
json={"name": "Test", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 404
def test_update_space_status(client: TestClient, admin_token: str) -> None:
"""Test activating/deactivating a space."""
# Create space
response = client.post(
"/api/admin/spaces",
json={"name": "Toggle Space", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
space_id = response.json()["id"]
# Deactivate
response = client.patch(
f"/api/admin/spaces/{space_id}/status",
json={"is_active": False},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert response.json()["is_active"] is False
# Reactivate
response = client.patch(
f"/api/admin/spaces/{space_id}/status",
json={"is_active": True},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert response.json()["is_active"] is True
def test_update_space_status_not_found(client: TestClient, admin_token: str) -> None:
"""Test updating status of non-existent space."""
response = client.patch(
"/api/admin/spaces/99999/status",
json={"is_active": False},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 404
# ===== Audit Log Integration Tests =====
def test_space_creation_creates_audit_log(
client: TestClient,
admin_token: str,
test_admin: User,
db: Session,
) -> None:
"""Test that creating a space creates an audit log entry."""
from app.models.audit_log import AuditLog
response = client.post(
"/api/admin/spaces",
json={
"name": "Conference Room A",
"type": "sala",
"capacity": 10,
"description": "Main conference room",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 201
space_id = response.json()["id"]
# Check audit log was created
audit = db.query(AuditLog).filter(
AuditLog.action == "space_created",
AuditLog.target_id == space_id
).first()
assert audit is not None
assert audit.target_type == "space"
assert audit.user_id == test_admin.id
assert audit.details == {"name": "Conference Room A", "type": "sala", "capacity": 10}
def test_space_update_creates_audit_log(
client: TestClient,
admin_token: str,
test_admin: User,
db: Session,
) -> None:
"""Test that updating a space creates an audit log entry."""
from app.models.audit_log import AuditLog
# Create space
response = client.post(
"/api/admin/spaces",
json={"name": "Original Name", "type": "sala", "capacity": 5},
headers={"Authorization": f"Bearer {admin_token}"},
)
space_id = response.json()["id"]
# Update space
response = client.put(
f"/api/admin/spaces/{space_id}",
json={
"name": "Updated Name",
"type": "birou",
"capacity": 2,
"description": "Updated description",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
# Check audit log was created
audit = db.query(AuditLog).filter(
AuditLog.action == "space_updated",
AuditLog.target_id == space_id
).first()
assert audit is not None
assert audit.target_type == "space"
assert audit.user_id == test_admin.id
# Should track all changed fields
assert "name" in audit.details["updated_fields"]
assert "type" in audit.details["updated_fields"]
assert "capacity" in audit.details["updated_fields"]
assert len(audit.details["updated_fields"]) == 4 # name, type, capacity, description