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>
324 lines
10 KiB
Python
324 lines
10 KiB
Python
"""Tests for booking templates API."""
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def test_create_template(client: TestClient, user_token: str, test_space):
|
|
"""Test creating a booking template."""
|
|
response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Weekly Team Sync",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 60,
|
|
"title": "Team Sync Meeting",
|
|
"description": "Weekly team synchronization meeting",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "Weekly Team Sync"
|
|
assert data["space_id"] == test_space.id
|
|
assert data["space_name"] == test_space.name
|
|
assert data["duration_minutes"] == 60
|
|
assert data["title"] == "Team Sync Meeting"
|
|
assert data["description"] == "Weekly team synchronization meeting"
|
|
assert data["usage_count"] == 0
|
|
|
|
|
|
def test_create_template_without_space(client: TestClient, user_token: str):
|
|
"""Test creating a template without a default space."""
|
|
response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Generic Meeting",
|
|
"duration_minutes": 30,
|
|
"title": "Meeting",
|
|
"description": "Generic meeting template",
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["name"] == "Generic Meeting"
|
|
assert data["space_id"] is None
|
|
assert data["space_name"] is None
|
|
|
|
|
|
def test_list_templates(client: TestClient, user_token: str, test_space):
|
|
"""Test listing user's templates."""
|
|
# Create two templates
|
|
client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Template 1",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 30,
|
|
"title": "Meeting 1",
|
|
},
|
|
)
|
|
client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Template 2",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 60,
|
|
"title": "Meeting 2",
|
|
},
|
|
)
|
|
|
|
# List templates
|
|
response = client.get(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) >= 2
|
|
assert any(t["name"] == "Template 1" for t in data)
|
|
assert any(t["name"] == "Template 2" for t in data)
|
|
|
|
|
|
def test_list_templates_isolated(client: TestClient, user_token: str, admin_token: str, test_space):
|
|
"""Test that users only see their own templates."""
|
|
# User creates a template
|
|
client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "User Template",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 30,
|
|
"title": "User Meeting",
|
|
},
|
|
)
|
|
|
|
# Admin creates a template
|
|
client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
json={
|
|
"name": "Admin Template",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 60,
|
|
"title": "Admin Meeting",
|
|
},
|
|
)
|
|
|
|
# User lists templates
|
|
user_response = client.get(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
user_data = user_response.json()
|
|
|
|
# Admin lists templates
|
|
admin_response = client.get(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
admin_data = admin_response.json()
|
|
|
|
# User should only see their template
|
|
user_template_names = [t["name"] for t in user_data]
|
|
assert "User Template" in user_template_names
|
|
assert "Admin Template" not in user_template_names
|
|
|
|
# Admin should only see their template
|
|
admin_template_names = [t["name"] for t in admin_data]
|
|
assert "Admin Template" in admin_template_names
|
|
assert "User Template" not in admin_template_names
|
|
|
|
|
|
def test_delete_template(client: TestClient, user_token: str, test_space):
|
|
"""Test deleting a template."""
|
|
# Create template
|
|
create_response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "To Delete",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 30,
|
|
"title": "Meeting",
|
|
},
|
|
)
|
|
template_id = create_response.json()["id"]
|
|
|
|
# Delete template
|
|
delete_response = client.delete(
|
|
f"/api/booking-templates/{template_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert delete_response.status_code == 204
|
|
|
|
# Verify it's gone
|
|
list_response = client.get(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
templates = list_response.json()
|
|
assert not any(t["id"] == template_id for t in templates)
|
|
|
|
|
|
def test_delete_template_not_found(client: TestClient, user_token: str):
|
|
"""Test deleting a non-existent template."""
|
|
response = client.delete(
|
|
"/api/booking-templates/99999",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_delete_template_other_user(client: TestClient, user_token: str, admin_token: str, test_space):
|
|
"""Test that users cannot delete other users' templates."""
|
|
# Admin creates a template
|
|
create_response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
json={
|
|
"name": "Admin Template",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 30,
|
|
"title": "Meeting",
|
|
},
|
|
)
|
|
template_id = create_response.json()["id"]
|
|
|
|
# User tries to delete admin's template
|
|
delete_response = client.delete(
|
|
f"/api/booking-templates/{template_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert delete_response.status_code == 404
|
|
|
|
|
|
def test_create_booking_from_template(client: TestClient, user_token: str, test_space):
|
|
"""Test creating a booking from a template."""
|
|
# Create template
|
|
template_response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Client Meeting",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 90,
|
|
"title": "Client Presentation",
|
|
"description": "Quarterly review with client",
|
|
},
|
|
)
|
|
template_id = template_response.json()["id"]
|
|
|
|
# Create booking from template
|
|
tomorrow = datetime.now() + timedelta(days=1)
|
|
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
|
|
|
|
response = client.post(
|
|
f"/api/booking-templates/from-template/{template_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
params={"start_datetime": start_time.isoformat()},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["space_id"] == test_space.id
|
|
assert data["title"] == "Client Presentation"
|
|
assert data["description"] == "Quarterly review with client"
|
|
assert data["status"] == "pending"
|
|
|
|
# Verify duration
|
|
start_dt = datetime.fromisoformat(data["start_datetime"])
|
|
end_dt = datetime.fromisoformat(data["end_datetime"])
|
|
duration = (end_dt - start_dt).total_seconds() / 60
|
|
assert duration == 90
|
|
|
|
# Verify usage count incremented
|
|
list_response = client.get(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
templates = list_response.json()
|
|
template = next(t for t in templates if t["id"] == template_id)
|
|
assert template["usage_count"] == 1
|
|
|
|
|
|
def test_create_booking_from_template_no_space(client: TestClient, user_token: str):
|
|
"""Test creating a booking from a template without a default space."""
|
|
# Create template without space
|
|
template_response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Generic Meeting",
|
|
"duration_minutes": 60,
|
|
"title": "Meeting",
|
|
},
|
|
)
|
|
template_id = template_response.json()["id"]
|
|
|
|
# Try to create booking
|
|
tomorrow = datetime.now() + timedelta(days=1)
|
|
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
|
|
|
|
response = client.post(
|
|
f"/api/booking-templates/from-template/{template_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
params={"start_datetime": start_time.isoformat()},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "does not have a default space" in response.json()["detail"]
|
|
|
|
|
|
def test_create_booking_from_template_not_found(client: TestClient, user_token: str):
|
|
"""Test creating a booking from a non-existent template."""
|
|
tomorrow = datetime.now() + timedelta(days=1)
|
|
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
|
|
|
|
response = client.post(
|
|
"/api/booking-templates/from-template/99999",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
params={"start_datetime": start_time.isoformat()},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_create_booking_from_template_validation_error(client: TestClient, user_token: str, test_space):
|
|
"""Test that booking from template validates booking rules."""
|
|
# Create template
|
|
template_response = client.post(
|
|
"/api/booking-templates",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"name": "Long Meeting",
|
|
"space_id": test_space.id,
|
|
"duration_minutes": 600, # 10 hours - exceeds max
|
|
"title": "Marathon Meeting",
|
|
},
|
|
)
|
|
template_id = template_response.json()["id"]
|
|
|
|
# Try to create booking
|
|
tomorrow = datetime.now() + timedelta(days=1)
|
|
start_time = tomorrow.replace(hour=10, minute=0, second=0, microsecond=0)
|
|
|
|
response = client.post(
|
|
f"/api/booking-templates/from-template/{template_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
params={"start_datetime": start_time.isoformat()},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
# Should fail validation (duration exceeds max)
|