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,296 @@
"""Test report endpoints."""
from datetime import datetime
import pytest
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.booking import Booking
from app.models.space import Space
from app.models.user import User
@pytest.fixture
def test_spaces(db: Session) -> list[Space]:
"""Create multiple test spaces."""
spaces = [
Space(
name="Conference Room A",
type="sala",
capacity=10,
description="Test room A",
is_active=True,
),
Space(
name="Office B",
type="birou",
capacity=2,
description="Test office B",
is_active=True,
),
]
for space in spaces:
db.add(space)
db.commit()
for space in spaces:
db.refresh(space)
return spaces
@pytest.fixture
def test_users(db: Session, test_user: User) -> list[User]:
"""Create multiple test users."""
from app.core.security import get_password_hash
user2 = User(
email="user2@example.com",
full_name="User Two",
hashed_password=get_password_hash("password"),
role="user",
is_active=True,
)
db.add(user2)
db.commit()
db.refresh(user2)
return [test_user, user2]
@pytest.fixture
def test_bookings(
db: Session, test_users: list[User], test_spaces: list[Space]
) -> list[Booking]:
"""Create multiple test bookings with various statuses."""
bookings = [
# User 1, Space 1, approved
Booking(
user_id=test_users[0].id,
space_id=test_spaces[0].id,
title="Meeting 1",
description="Test",
start_datetime=datetime(2024, 3, 15, 10, 0),
end_datetime=datetime(2024, 3, 15, 12, 0), # 2 hours
status="approved",
),
# User 1, Space 1, pending
Booking(
user_id=test_users[0].id,
space_id=test_spaces[0].id,
title="Meeting 2",
description="Test",
start_datetime=datetime(2024, 3, 16, 10, 0),
end_datetime=datetime(2024, 3, 16, 11, 0), # 1 hour
status="pending",
),
# User 1, Space 2, rejected
Booking(
user_id=test_users[0].id,
space_id=test_spaces[1].id,
title="Meeting 3",
description="Test",
start_datetime=datetime(2024, 3, 17, 10, 0),
end_datetime=datetime(2024, 3, 17, 13, 0), # 3 hours
status="rejected",
rejection_reason="Conflict",
),
# User 2, Space 1, approved
Booking(
user_id=test_users[1].id,
space_id=test_spaces[0].id,
title="Meeting 4",
description="Test",
start_datetime=datetime(2024, 3, 18, 10, 0),
end_datetime=datetime(2024, 3, 18, 14, 0), # 4 hours
status="approved",
),
# User 2, Space 1, canceled
Booking(
user_id=test_users[1].id,
space_id=test_spaces[0].id,
title="Meeting 5",
description="Test",
start_datetime=datetime(2024, 3, 19, 10, 0),
end_datetime=datetime(2024, 3, 19, 11, 30), # 1.5 hours
status="canceled",
cancellation_reason="Not needed",
),
]
for booking in bookings:
db.add(booking)
db.commit()
for booking in bookings:
db.refresh(booking)
return bookings
def test_usage_report(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
) -> None:
"""Test usage report generation."""
response = client.get("/api/admin/reports/usage", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "total_bookings" in data
assert "date_range" in data
# Should have 2 spaces
assert len(data["items"]) == 2
# Check Conference Room A
conf_room = next((i for i in data["items"] if i["space_name"] == "Conference Room A"), None)
assert conf_room is not None
assert conf_room["total_bookings"] == 4 # 4 bookings in this space
assert conf_room["approved_bookings"] == 2
assert conf_room["pending_bookings"] == 1
assert conf_room["rejected_bookings"] == 0
assert conf_room["canceled_bookings"] == 1
assert conf_room["total_hours"] == 8.5 # 2 + 1 + 4 + 1.5
# Check Office B
office = next((i for i in data["items"] if i["space_name"] == "Office B"), None)
assert office is not None
assert office["total_bookings"] == 1
assert office["rejected_bookings"] == 1
assert office["total_hours"] == 3.0
# Total bookings across all spaces
assert data["total_bookings"] == 5
def test_usage_report_with_date_filter(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
) -> None:
"""Test usage report with date range filter."""
# Filter for March 15-16 only
response = client.get(
"/api/admin/reports/usage",
headers=admin_headers,
params={"start_date": "2024-03-15", "end_date": "2024-03-16"},
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1 # Only Conference Room A
assert data["total_bookings"] == 2 # Meeting 1 and 2
def test_usage_report_with_space_filter(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
test_spaces: list[Space],
) -> None:
"""Test usage report with space filter."""
response = client.get(
"/api/admin/reports/usage",
headers=admin_headers,
params={"space_id": test_spaces[0].id},
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1
assert data["items"][0]["space_name"] == "Conference Room A"
def test_top_users_report(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
test_users: list[User],
) -> None:
"""Test top users report."""
response = client.get("/api/admin/reports/top-users", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "date_range" in data
# Should have 2 users
assert len(data["items"]) == 2
# Top user should be test_user with 3 bookings
assert data["items"][0]["user_email"] == test_users[0].email
assert data["items"][0]["total_bookings"] == 3
assert data["items"][0]["approved_bookings"] == 1
assert data["items"][0]["total_hours"] == 6.0 # 2 + 1 + 3
# Second user with 2 bookings
assert data["items"][1]["user_email"] == test_users[1].email
assert data["items"][1]["total_bookings"] == 2
assert data["items"][1]["approved_bookings"] == 1
assert data["items"][1]["total_hours"] == 5.5 # 4 + 1.5
def test_top_users_report_with_limit(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
) -> None:
"""Test top users report with limit."""
response = client.get(
"/api/admin/reports/top-users",
headers=admin_headers,
params={"limit": 1},
)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 1 # Only top user
def test_approval_rate_report(
client: TestClient,
admin_headers: dict[str, str],
test_bookings: list[Booking],
) -> None:
"""Test approval rate report."""
response = client.get("/api/admin/reports/approval-rate", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["total_requests"] == 5
assert data["approved"] == 2
assert data["rejected"] == 1
assert data["pending"] == 1
assert data["canceled"] == 1
# Approval rate = 2 / (2 + 1) = 66.67%
assert data["approval_rate"] == 66.67
# Rejection rate = 1 / (2 + 1) = 33.33%
assert data["rejection_rate"] == 33.33
def test_reports_require_admin(
client: TestClient, auth_headers: dict[str, str]
) -> None:
"""Test that regular users cannot access reports."""
endpoints = [
"/api/admin/reports/usage",
"/api/admin/reports/top-users",
"/api/admin/reports/approval-rate",
]
for endpoint in endpoints:
response = client.get(endpoint, headers=auth_headers)
assert response.status_code == 403
assert response.json()["detail"] == "Not enough permissions"
def test_reports_require_auth(client: TestClient) -> None:
"""Test that reports require authentication."""
endpoints = [
"/api/admin/reports/usage",
"/api/admin/reports/top-users",
"/api/admin/reports/approval-rate",
]
for endpoint in endpoints:
response = client.get(endpoint)
assert response.status_code == 403