Files
space-booking/backend/tests/test_recurring_bookings.py
Claude Agent df4031d99c 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>
2026-02-09 17:51:29 +00:00

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"]