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_recurring_bookings.py
Normal file
328
backend/tests/test_recurring_bookings.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user