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:
328
backend/tests/test_spaces.py
Normal file
328
backend/tests/test_spaces.py
Normal 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
|
||||
Reference in New Issue
Block a user