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

251
backend/tests/test_users.py Normal file
View File

@@ -0,0 +1,251 @@
"""Tests for user management endpoints."""
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.models.user import User
def test_get_current_user(client: TestClient, test_user: User, auth_headers: dict) -> None:
"""Test getting current user info."""
response = client.get("/api/users/me", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["email"] == "test@example.com"
assert data["role"] == "user"
def test_list_users_admin(
client: TestClient, test_user: User, admin_headers: dict
) -> None:
"""Test listing all users as admin."""
response = client.get("/api/admin/users", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert isinstance(data, list)
assert len(data) >= 2 # test_user + admin_user
def test_list_users_unauthorized(client: TestClient, auth_headers: dict) -> None:
"""Test listing users as non-admin (should fail)."""
response = client.get("/api/admin/users", headers=auth_headers)
assert response.status_code == 403
def test_list_users_filter_by_role(client: TestClient, admin_headers: dict) -> None:
"""Test filtering users by role."""
response = client.get("/api/admin/users?role=admin", headers=admin_headers)
assert response.status_code == 200
data = response.json()
assert all(user["role"] == "admin" for user in data)
def test_create_user_admin(client: TestClient, admin_headers: dict) -> None:
"""Test creating a new user as admin."""
response = client.post(
"/api/admin/users",
headers=admin_headers,
json={
"email": "newuser@example.com",
"full_name": "New User",
"password": "newpassword",
"role": "user",
"organization": "Test Org",
},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "newuser@example.com"
assert data["full_name"] == "New User"
assert data["role"] == "user"
assert data["organization"] == "Test Org"
assert data["is_active"] is True
def test_create_user_duplicate_email(
client: TestClient, test_user: User, admin_headers: dict
) -> None:
"""Test creating user with duplicate email."""
response = client.post(
"/api/admin/users",
headers=admin_headers,
json={
"email": "test@example.com", # Already exists
"full_name": "Duplicate User",
"password": "password",
"role": "user",
},
)
assert response.status_code == 400
assert "already exists" in response.json()["detail"]
def test_create_user_invalid_role(client: TestClient, admin_headers: dict) -> None:
"""Test creating user with invalid role."""
response = client.post(
"/api/admin/users",
headers=admin_headers,
json={
"email": "invalid@example.com",
"full_name": "Invalid User",
"password": "password",
"role": "superadmin", # Invalid role
},
)
assert response.status_code == 400
assert "admin" in response.json()["detail"].lower()
def test_update_user_admin(client: TestClient, test_user: User, admin_headers: dict) -> None:
"""Test updating user as admin."""
response = client.put(
f"/api/admin/users/{test_user.id}",
headers=admin_headers,
json={
"full_name": "Updated Name",
"organization": "Updated Org",
},
)
assert response.status_code == 200
data = response.json()
assert data["full_name"] == "Updated Name"
assert data["organization"] == "Updated Org"
assert data["email"] == "test@example.com" # Should remain unchanged
def test_update_user_not_found(client: TestClient, admin_headers: dict) -> None:
"""Test updating non-existent user."""
response = client.put(
"/api/admin/users/99999",
headers=admin_headers,
json={"full_name": "Updated"},
)
assert response.status_code == 404
def test_update_user_status(client: TestClient, test_user: User, admin_headers: dict) -> None:
"""Test deactivating user."""
response = client.patch(
f"/api/admin/users/{test_user.id}/status",
headers=admin_headers,
json={"is_active": False},
)
assert response.status_code == 200
data = response.json()
assert data["is_active"] is False
# Verify user cannot login
login_response = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "testpassword"},
)
assert login_response.status_code == 403
def test_reset_password(
client: TestClient, test_user: User, admin_headers: dict, db: Session
) -> None:
"""Test resetting user password."""
response = client.post(
f"/api/admin/users/{test_user.id}/reset-password",
headers=admin_headers,
json={"new_password": "newpassword123"},
)
assert response.status_code == 200
# Verify new password works
login_response = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "newpassword123"},
)
assert login_response.status_code == 200
# Verify old password doesn't work
old_login_response = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "testpassword"},
)
assert old_login_response.status_code == 401
def test_reset_password_not_found(client: TestClient, admin_headers: dict) -> None:
"""Test resetting password for non-existent user."""
response = client.post(
"/api/admin/users/99999/reset-password",
headers=admin_headers,
json={"new_password": "newpassword"},
)
assert response.status_code == 404
# ===== Audit Log Integration Tests =====
def test_user_creation_creates_audit_log(
client: TestClient,
admin_token: str,
test_admin: User,
db: Session,
) -> None:
"""Test that creating a user creates an audit log entry."""
from app.models.audit_log import AuditLog
response = client.post(
"/api/admin/users",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"email": "newuser@example.com",
"full_name": "New User",
"password": "newpassword",
"role": "user",
"organization": "Test Org",
},
)
assert response.status_code == 201
user_id = response.json()["id"]
# Check audit log was created
audit = db.query(AuditLog).filter(
AuditLog.action == "user_created",
AuditLog.target_id == user_id
).first()
assert audit is not None
assert audit.target_type == "user"
assert audit.user_id == test_admin.id
assert audit.details == {"email": "newuser@example.com", "role": "user"}
def test_user_update_creates_audit_log(
client: TestClient,
admin_token: str,
test_admin: User,
test_user: User,
db: Session,
) -> None:
"""Test that updating a user creates an audit log entry."""
from app.models.audit_log import AuditLog
response = client.put(
f"/api/admin/users/{test_user.id}",
headers={"Authorization": f"Bearer {admin_token}"},
json={
"full_name": "Updated Name",
"role": "admin",
},
)
assert response.status_code == 200
# Check audit log was created
audit = db.query(AuditLog).filter(
AuditLog.action == "user_updated",
AuditLog.target_id == test_user.id
).first()
assert audit is not None
assert audit.target_type == "user"
assert audit.user_id == test_admin.id
# Should track changed fields
assert "full_name" in audit.details["updated_fields"]
assert "role" in audit.details["updated_fields"]