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,241 @@
"""Tests for settings endpoints."""
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.settings import Settings
from app.models.user import User
def test_get_settings_admin(
client: TestClient,
db: Session,
admin_headers: dict[str, str],
) -> None:
"""Test GET /api/admin/settings returns settings for admin."""
# Create default settings
settings = Settings(
id=1,
min_duration_minutes=30,
max_duration_minutes=480,
working_hours_start=8,
working_hours_end=20,
max_bookings_per_day_per_user=3,
min_hours_before_cancel=2,
)
db.add(settings)
db.commit()
response = client.get("/api/admin/settings", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["min_duration_minutes"] == 30
assert data["max_duration_minutes"] == 480
assert data["working_hours_start"] == 8
assert data["working_hours_end"] == 20
assert data["max_bookings_per_day_per_user"] == 3
assert data["min_hours_before_cancel"] == 2
def test_get_settings_creates_default_if_not_exist(
client: TestClient,
db: Session,
admin_headers: dict[str, str],
) -> None:
"""Test GET /api/admin/settings creates default settings if not exist."""
response = client.get("/api/admin/settings", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert data["min_duration_minutes"] == 30
assert data["max_duration_minutes"] == 480
# Verify it was created in DB
settings = db.query(Settings).filter(Settings.id == 1).first()
assert settings is not None
assert settings.min_duration_minutes == 30
def test_get_settings_non_admin_forbidden(
client: TestClient,
auth_headers: dict[str, str],
) -> None:
"""Test GET /api/admin/settings forbidden for non-admin."""
response = client.get("/api/admin/settings", headers=auth_headers)
assert response.status_code == 403
def test_update_settings_admin(
client: TestClient,
db: Session,
admin_headers: dict[str, str],
) -> None:
"""Test PUT /api/admin/settings updates settings for admin."""
# Create default settings
settings = Settings(
id=1,
min_duration_minutes=30,
max_duration_minutes=480,
working_hours_start=8,
working_hours_end=20,
max_bookings_per_day_per_user=3,
min_hours_before_cancel=2,
)
db.add(settings)
db.commit()
# Update settings
update_data = {
"min_duration_minutes": 60,
"max_duration_minutes": 600,
"working_hours_start": 9,
"working_hours_end": 18,
"max_bookings_per_day_per_user": 5,
"min_hours_before_cancel": 4,
}
response = client.put("/api/admin/settings", headers=admin_headers, json=update_data)
assert response.status_code == 200
data = response.json()
assert data["min_duration_minutes"] == 60
assert data["max_duration_minutes"] == 600
assert data["working_hours_start"] == 9
assert data["working_hours_end"] == 18
assert data["max_bookings_per_day_per_user"] == 5
assert data["min_hours_before_cancel"] == 4
# Verify update in DB
db.refresh(settings)
assert settings.min_duration_minutes == 60
assert settings.max_bookings_per_day_per_user == 5
def test_update_settings_validation_min_max_duration(
client: TestClient,
db: Session,
admin_headers: dict[str, str],
) -> None:
"""Test PUT /api/admin/settings validates min <= max duration."""
# Create default settings
settings = Settings(id=1)
db.add(settings)
db.commit()
# Try to set min > max (but within Pydantic range)
update_data = {
"min_duration_minutes": 400,
"max_duration_minutes": 100,
"working_hours_start": 8,
"working_hours_end": 20,
"max_bookings_per_day_per_user": 3,
"min_hours_before_cancel": 2,
}
response = client.put("/api/admin/settings", headers=admin_headers, json=update_data)
assert response.status_code == 400
assert "duration" in response.json()["detail"].lower()
def test_update_settings_validation_working_hours(
client: TestClient,
db: Session,
admin_headers: dict[str, str],
) -> None:
"""Test PUT /api/admin/settings validates start < end hours."""
# Create default settings
settings = Settings(id=1)
db.add(settings)
db.commit()
# Try to set start >= end
update_data = {
"min_duration_minutes": 30,
"max_duration_minutes": 480,
"working_hours_start": 20,
"working_hours_end": 8,
"max_bookings_per_day_per_user": 3,
"min_hours_before_cancel": 2,
}
response = client.put("/api/admin/settings", headers=admin_headers, json=update_data)
assert response.status_code == 400
assert "working hours" in response.json()["detail"].lower()
def test_update_settings_non_admin_forbidden(
client: TestClient,
auth_headers: dict[str, str],
) -> None:
"""Test PUT /api/admin/settings forbidden for non-admin."""
update_data = {
"min_duration_minutes": 60,
"max_duration_minutes": 600,
"working_hours_start": 9,
"working_hours_end": 18,
"max_bookings_per_day_per_user": 5,
"min_hours_before_cancel": 4,
}
response = client.put("/api/admin/settings", headers=auth_headers, json=update_data)
assert response.status_code == 403
# ===== Audit Log Integration Tests =====
def test_settings_update_creates_audit_log(
client: TestClient,
admin_token: str,
test_admin: User,
db: Session,
) -> None:
"""Test that updating settings creates an audit log entry with changed fields."""
from app.models.audit_log import AuditLog
# Create default settings
settings = Settings(
id=1,
min_duration_minutes=30,
max_duration_minutes=480,
working_hours_start=8,
working_hours_end=20,
max_bookings_per_day_per_user=3,
min_hours_before_cancel=2,
)
db.add(settings)
db.commit()
# Update settings
update_data = {
"min_duration_minutes": 60,
"max_duration_minutes": 600,
"working_hours_start": 9,
"working_hours_end": 18,
"max_bookings_per_day_per_user": 5,
"min_hours_before_cancel": 4,
}
response = client.put(
"/api/admin/settings",
headers={"Authorization": f"Bearer {admin_token}"},
json=update_data
)
assert response.status_code == 200
# Check audit log was created
audit = db.query(AuditLog).filter(
AuditLog.action == "settings_updated",
AuditLog.target_id == 1
).first()
assert audit is not None
assert audit.target_type == "settings"
assert audit.user_id == test_admin.id
# Check that changed fields are tracked with old and new values
changed_fields = audit.details["changed_fields"]
assert "min_duration_minutes" in changed_fields
assert changed_fields["min_duration_minutes"]["old"] == 30
assert changed_fields["min_duration_minutes"]["new"] == 60
assert "max_duration_minutes" in changed_fields
assert changed_fields["max_duration_minutes"]["old"] == 480
assert changed_fields["max_duration_minutes"]["new"] == 600
assert "working_hours_start" in changed_fields
assert "working_hours_end" in changed_fields
assert "max_bookings_per_day_per_user" in changed_fields
assert "min_hours_before_cancel" in changed_fields
assert len(changed_fields) == 6 # All 6 fields changed