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:
338
backend/tests/test_booking_service.py
Normal file
338
backend/tests/test_booking_service.py
Normal file
@@ -0,0 +1,338 @@
|
||||
"""Tests for booking validation service."""
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.booking import Booking
|
||||
from app.models.settings import Settings
|
||||
from app.models.space import Space
|
||||
from app.models.user import User
|
||||
from app.services.booking_service import validate_booking_rules
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_settings(db: Session) -> Settings:
|
||||
"""Create test settings."""
|
||||
settings = Settings(
|
||||
id=1,
|
||||
min_duration_minutes=30,
|
||||
max_duration_minutes=480, # 8 hours
|
||||
working_hours_start=8,
|
||||
working_hours_end=20,
|
||||
max_bookings_per_day_per_user=3,
|
||||
min_hours_before_cancel=2,
|
||||
)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
return settings
|
||||
|
||||
|
||||
def test_validate_duration_too_short(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails for booking duration too short."""
|
||||
start = datetime(2024, 3, 15, 10, 0, 0)
|
||||
end = datetime(2024, 3, 15, 10, 15, 0) # Only 15 minutes (min is 30)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Durata rezervării trebuie să fie între 30 și 480 minute" in errors[0]
|
||||
|
||||
|
||||
def test_validate_duration_too_long(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails for booking duration too long."""
|
||||
start = datetime(2024, 3, 15, 8, 0, 0)
|
||||
end = datetime(2024, 3, 15, 20, 0, 0) # 12 hours = 720 minutes (max is 480)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Durata rezervării trebuie să fie între 30 și 480 minute" in errors[0]
|
||||
|
||||
|
||||
def test_validate_outside_working_hours_start(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails for booking starting before working hours."""
|
||||
start = datetime(2024, 3, 15, 7, 0, 0) # Before 8 AM
|
||||
end = datetime(2024, 3, 15, 9, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Rezervările sunt permise doar între 8:00 și 20:00" in errors[0]
|
||||
|
||||
|
||||
def test_validate_outside_working_hours_end(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails for booking ending after working hours."""
|
||||
start = datetime(2024, 3, 15, 19, 0, 0)
|
||||
end = datetime(2024, 3, 15, 21, 0, 0) # After 8 PM
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Rezervările sunt permise doar între 8:00 și 20:00" in errors[0]
|
||||
|
||||
|
||||
def test_validate_overlap_detected_pending(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails when space is already booked (pending status)."""
|
||||
# Create existing booking
|
||||
existing = Booking(
|
||||
user_id=test_user.id,
|
||||
space_id=test_space.id,
|
||||
title="Existing Meeting",
|
||||
start_datetime=datetime(2024, 3, 15, 10, 0, 0),
|
||||
end_datetime=datetime(2024, 3, 15, 12, 0, 0),
|
||||
status="pending",
|
||||
)
|
||||
db.add(existing)
|
||||
db.commit()
|
||||
|
||||
# Try to create overlapping booking
|
||||
start = datetime(2024, 3, 15, 11, 0, 0)
|
||||
end = datetime(2024, 3, 15, 13, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Spațiul este deja rezervat în acest interval" in errors[0]
|
||||
|
||||
|
||||
def test_validate_overlap_detected_approved(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails when space is already booked (approved status)."""
|
||||
# Create existing booking
|
||||
existing = Booking(
|
||||
user_id=test_user.id,
|
||||
space_id=test_space.id,
|
||||
title="Existing Meeting",
|
||||
start_datetime=datetime(2024, 3, 15, 10, 0, 0),
|
||||
end_datetime=datetime(2024, 3, 15, 12, 0, 0),
|
||||
status="approved",
|
||||
)
|
||||
db.add(existing)
|
||||
db.commit()
|
||||
|
||||
# Try to create overlapping booking
|
||||
start = datetime(2024, 3, 15, 11, 0, 0)
|
||||
end = datetime(2024, 3, 15, 13, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Spațiul este deja rezervat în acest interval" in errors[0]
|
||||
|
||||
|
||||
def test_validate_no_overlap_rejected(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation passes when existing booking is rejected."""
|
||||
# Create rejected booking
|
||||
existing = Booking(
|
||||
user_id=test_user.id,
|
||||
space_id=test_space.id,
|
||||
title="Rejected Meeting",
|
||||
start_datetime=datetime(2024, 3, 15, 10, 0, 0),
|
||||
end_datetime=datetime(2024, 3, 15, 12, 0, 0),
|
||||
status="rejected",
|
||||
)
|
||||
db.add(existing)
|
||||
db.commit()
|
||||
|
||||
# Try to create booking in same time slot
|
||||
start = datetime(2024, 3, 15, 11, 0, 0)
|
||||
end = datetime(2024, 3, 15, 12, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
# Should have no overlap error (rejected bookings don't count)
|
||||
assert "Spațiul este deja rezervat în acest interval" not in errors
|
||||
|
||||
|
||||
def test_validate_max_bookings_exceeded(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation fails when user exceeds max bookings per day."""
|
||||
# Create 3 bookings for the same day (max is 3)
|
||||
base_date = datetime(2024, 3, 15)
|
||||
for i in range(3):
|
||||
booking = Booking(
|
||||
user_id=test_user.id,
|
||||
space_id=test_space.id,
|
||||
title=f"Meeting {i+1}",
|
||||
start_datetime=base_date.replace(hour=9 + i * 2),
|
||||
end_datetime=base_date.replace(hour=10 + i * 2),
|
||||
status="approved",
|
||||
)
|
||||
db.add(booking)
|
||||
db.commit()
|
||||
|
||||
# Try to create 4th booking on same day
|
||||
start = datetime(2024, 3, 15, 16, 0, 0)
|
||||
end = datetime(2024, 3, 15, 17, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "Ai atins limita de 3 rezervări pe zi" in errors[0]
|
||||
|
||||
|
||||
def test_validate_max_bookings_different_day_ok(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation passes when max bookings reached on different day."""
|
||||
# Create 3 bookings for previous day
|
||||
previous_date = datetime(2024, 3, 14)
|
||||
for i in range(3):
|
||||
booking = Booking(
|
||||
user_id=test_user.id,
|
||||
space_id=test_space.id,
|
||||
title=f"Meeting {i+1}",
|
||||
start_datetime=previous_date.replace(hour=9 + i * 2),
|
||||
end_datetime=previous_date.replace(hour=10 + i * 2),
|
||||
status="approved",
|
||||
)
|
||||
db.add(booking)
|
||||
db.commit()
|
||||
|
||||
# Try to create booking on different day
|
||||
start = datetime(2024, 3, 15, 10, 0, 0)
|
||||
end = datetime(2024, 3, 15, 11, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
# Should have no max bookings error (different day)
|
||||
assert "Ai atins limita de 3 rezervări pe zi" not in errors
|
||||
|
||||
|
||||
def test_validate_all_rules_pass(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation passes when all rules are satisfied (happy path)."""
|
||||
start = datetime(2024, 3, 15, 10, 0, 0)
|
||||
end = datetime(2024, 3, 15, 11, 0, 0) # 1 hour duration
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 0
|
||||
|
||||
|
||||
def test_validate_multiple_errors(
|
||||
db: Session, test_user: User, test_space: Space, test_settings: Settings
|
||||
):
|
||||
"""Test validation returns multiple errors when multiple rules fail."""
|
||||
# Duration too short AND outside working hours
|
||||
start = datetime(2024, 3, 15, 6, 0, 0) # Before 8 AM
|
||||
end = datetime(2024, 3, 15, 6, 10, 0) # Only 10 minutes
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
assert len(errors) == 2
|
||||
assert any("Durata rezervării" in error for error in errors)
|
||||
assert any("Rezervările sunt permise doar" in error for error in errors)
|
||||
|
||||
|
||||
def test_validate_creates_default_settings(db: Session, test_user: User, test_space: Space):
|
||||
"""Test validation creates default settings if they don't exist."""
|
||||
# Ensure no settings exist
|
||||
db.query(Settings).delete()
|
||||
db.commit()
|
||||
|
||||
start = datetime(2024, 3, 15, 10, 0, 0)
|
||||
end = datetime(2024, 3, 15, 11, 0, 0)
|
||||
|
||||
errors = validate_booking_rules(
|
||||
db=db,
|
||||
space_id=test_space.id,
|
||||
user_id=test_user.id,
|
||||
start_datetime=start,
|
||||
end_datetime=end,
|
||||
)
|
||||
|
||||
# Verify settings were created
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
assert settings is not None
|
||||
assert settings.min_duration_minutes == 30
|
||||
assert settings.max_duration_minutes == 480
|
||||
assert settings.working_hours_start == 8
|
||||
assert settings.working_hours_end == 20
|
||||
assert settings.max_bookings_per_day_per_user == 3
|
||||
|
||||
# Should pass validation with default settings
|
||||
assert len(errors) == 0
|
||||
Reference in New Issue
Block a user