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>
297 lines
8.8 KiB
Python
297 lines
8.8 KiB
Python
"""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
|