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>
329 lines
11 KiB
Python
329 lines
11 KiB
Python
"""Tests for recurring booking endpoints."""
|
|
from datetime import date, timedelta
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.booking import Booking
|
|
|
|
|
|
def test_create_recurring_booking_success(
|
|
client: TestClient, user_token: str, test_space, db: Session
|
|
):
|
|
"""Test successful creation of recurring bookings."""
|
|
# Create 4 Monday bookings (4 weeks)
|
|
start_date = date.today() + timedelta(days=7) # Next week
|
|
# Find the next Monday
|
|
while start_date.weekday() != 0: # 0 = Monday
|
|
start_date += timedelta(days=1)
|
|
|
|
end_date = start_date + timedelta(days=21) # 3 weeks later
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Weekly Team Sync",
|
|
"description": "Regular team meeting",
|
|
"recurrence_days": [0], # Monday only
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["total_requested"] == 4
|
|
assert data["total_created"] == 4
|
|
assert data["total_skipped"] == 0
|
|
assert len(data["created_bookings"]) == 4
|
|
assert len(data["skipped_dates"]) == 0
|
|
|
|
# Verify all bookings were created
|
|
bookings = db.query(Booking).filter(Booking.title == "Weekly Team Sync").all()
|
|
assert len(bookings) == 4
|
|
|
|
|
|
def test_create_recurring_booking_multiple_days(
|
|
client: TestClient, user_token: str, test_space, db: Session
|
|
):
|
|
"""Test recurring booking on multiple days (Mon, Wed, Fri)."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
# Find the next Monday
|
|
while start_date.weekday() != 0:
|
|
start_date += timedelta(days=1)
|
|
|
|
end_date = start_date + timedelta(days=14) # 2 weeks
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "14:00",
|
|
"duration_minutes": 90,
|
|
"title": "MWF Sessions",
|
|
"recurrence_days": [0, 2, 4], # Mon, Wed, Fri
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
# Could be 6 or 7 depending on whether start_date itself is included
|
|
assert data["total_created"] >= 6
|
|
assert data["total_created"] <= 7
|
|
assert data["total_skipped"] == 0
|
|
|
|
|
|
def test_create_recurring_booking_skip_conflicts(
|
|
client: TestClient, user_token: str, test_space, admin_token: str, db: Session
|
|
):
|
|
"""Test skipping conflicted dates."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
# Find the next Monday
|
|
while start_date.weekday() != 0:
|
|
start_date += timedelta(days=1)
|
|
|
|
# Create a conflicting booking on the 2nd Monday
|
|
conflict_date = start_date + timedelta(days=7)
|
|
|
|
client.post(
|
|
"/api/bookings",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_datetime": f"{conflict_date.isoformat()}T10:00:00",
|
|
"end_datetime": f"{conflict_date.isoformat()}T11:00:00",
|
|
"title": "Conflicting Booking",
|
|
},
|
|
)
|
|
|
|
# Now create recurring booking that will hit the conflict
|
|
end_date = start_date + timedelta(days=21)
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Weekly Recurring",
|
|
"recurrence_days": [0], # Monday
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["total_requested"] == 4
|
|
assert data["total_created"] == 3 # One skipped
|
|
assert data["total_skipped"] == 1
|
|
assert len(data["skipped_dates"]) == 1
|
|
assert data["skipped_dates"][0]["date"] == conflict_date.isoformat()
|
|
assert "deja rezervat" in data["skipped_dates"][0]["reason"].lower()
|
|
|
|
|
|
def test_create_recurring_booking_stop_on_conflict(
|
|
client: TestClient, user_token: str, test_space, db: Session
|
|
):
|
|
"""Test stopping on first conflict."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
# Find the next Monday
|
|
while start_date.weekday() != 0:
|
|
start_date += timedelta(days=1)
|
|
|
|
# Create a conflicting booking on the 2nd Monday
|
|
conflict_date = start_date + timedelta(days=7)
|
|
|
|
client.post(
|
|
"/api/bookings",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_datetime": f"{conflict_date.isoformat()}T10:00:00",
|
|
"end_datetime": f"{conflict_date.isoformat()}T11:00:00",
|
|
"title": "Conflicting Booking",
|
|
},
|
|
)
|
|
|
|
# Now create recurring booking with skip_conflicts=False
|
|
end_date = start_date + timedelta(days=21)
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Weekly Recurring",
|
|
"recurrence_days": [0],
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": False, # Stop on conflict
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["total_requested"] == 4
|
|
assert data["total_created"] == 1 # Only first one before conflict
|
|
assert data["total_skipped"] == 1
|
|
|
|
|
|
def test_create_recurring_booking_max_occurrences(
|
|
client: TestClient, user_token: str, test_space
|
|
):
|
|
"""Test limiting to 52 occurrences."""
|
|
start_date = date.today() + timedelta(days=1)
|
|
# Request 52+ weeks but within 1 year limit (365 days)
|
|
end_date = start_date + timedelta(days=364) # Within 1 year
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Long Recurring",
|
|
"recurrence_days": [0], # Monday
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
# Should be capped at 52 occurrences
|
|
assert data["total_requested"] <= 52
|
|
assert data["total_created"] <= 52
|
|
|
|
|
|
def test_create_recurring_booking_validation(client: TestClient, user_token: str):
|
|
"""Test validation (invalid days, date range, etc.)."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
end_date = start_date + timedelta(days=14)
|
|
|
|
# Invalid recurrence day (> 6)
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": 1,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Invalid Day",
|
|
"recurrence_days": [0, 7], # 7 is invalid
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert "Days must be 0-6" in response.json()["detail"][0]["msg"]
|
|
|
|
# End date before start date
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": 1,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Invalid Range",
|
|
"recurrence_days": [0],
|
|
"start_date": end_date.isoformat(),
|
|
"end_date": start_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert "end_date must be after start_date" in response.json()["detail"][0]["msg"]
|
|
|
|
# Date range > 1 year
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": 1,
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Too Long",
|
|
"recurrence_days": [0],
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": (start_date + timedelta(days=400)).isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 422
|
|
assert "cannot exceed 1 year" in response.json()["detail"][0]["msg"]
|
|
|
|
|
|
def test_create_recurring_booking_invalid_time_format(
|
|
client: TestClient, user_token: str, test_space
|
|
):
|
|
"""Test invalid time format."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
end_date = start_date + timedelta(days=14)
|
|
|
|
# Test with malformed time string
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": test_space.id,
|
|
"start_time": "abc", # Invalid format
|
|
"duration_minutes": 60,
|
|
"title": "Invalid Time",
|
|
"recurrence_days": [0],
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "Invalid start_time format" in response.json()["detail"]
|
|
|
|
|
|
def test_create_recurring_booking_space_not_found(
|
|
client: TestClient, user_token: str
|
|
):
|
|
"""Test recurring booking with non-existent space."""
|
|
start_date = date.today() + timedelta(days=7)
|
|
end_date = start_date + timedelta(days=14)
|
|
|
|
response = client.post(
|
|
"/api/bookings/recurring",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
json={
|
|
"space_id": 99999, # Non-existent
|
|
"start_time": "10:00",
|
|
"duration_minutes": 60,
|
|
"title": "Test",
|
|
"recurrence_days": [0],
|
|
"start_date": start_date.isoformat(),
|
|
"end_date": end_date.isoformat(),
|
|
"skip_conflicts": True,
|
|
},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
assert "Space not found" in response.json()["detail"]
|