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:
Claude Agent
2026-02-09 17:51:29 +00:00
commit df4031d99c
113 changed files with 24491 additions and 0 deletions

View 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