Files
eventspace/backend/tests/api/routes/events/test_router.py
Felipe Cardoso fee7d8b5ec Refactor CRUD module imports and usage for clarity.
Replaces ambiguous shorthand references like `event` and `user` with more descriptive names such as `event_crud` and `user_crud`. Updates imports, function calls, tests, and other references across the codebase to maintain consistency. This improves code readability and reduces potential confusion.
2025-03-15 01:22:04 +01:00

646 lines
23 KiB
Python

import uuid
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from typing import Optional, List
from uuid import UUID, uuid4
import pytest
from fastapi import status
from app.api.routes.events.router import events_router
from app.models.event import Event
from app.models import EventManager
from app.models import User
from app.models.event_manager import EventManagerRole, ROLE_PERMISSIONS
from app.models import Guest
@pytest.fixture
def event_data():
future_date = datetime.now(tz=timezone.utc) + timedelta(days=30)
slug = f"test-event-{uuid4().hex[:8]}"
return {
"title": "Test Event",
"slug": slug,
"description": "Test description",
"event_date": future_date.isoformat(),
"timezone": "UTC",
"is_public": True
}
@pytest.fixture
def past_event_data():
past_date = datetime.now(tz=timezone.utc) - timedelta(days=1)
slug = f"past-event-{uuid4().hex[:8]}"
return {
"title": "Past Event",
"slug": slug,
"description": "This event date is in the past",
"event_date": past_date.isoformat(),
"timezone": "UTC",
"is_public": True
}
@pytest.fixture
def invalid_slug_event_data():
future_date = datetime.now(tz=timezone.utc) + timedelta(days=30)
return {
"title": "Invalid Slug Event",
"slug": "INVALID Slug!!",
"description": "Event with invalid slug",
"event_date": future_date.isoformat(),
"timezone": "UTC",
"is_public": True
}
class TestCreateEvent:
"""Test scenarios for the create_event endpoint."""
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user):
self.client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
def test_create_event_success(self, db_session, mock_user, event_data):
response = self.client.post("/events/", json=event_data)
assert response.status_code == status.HTTP_201_CREATED, response.json()
resp_json = response.json()
assert resp_json["title"] == event_data["title"]
assert resp_json["slug"] == event_data["slug"]
event_id = UUID(resp_json["id"])
db_event = db_session.query(Event).filter(Event.id == event_id).first()
assert db_event is not None
def test_create_event_missing_required_fields(self, db_session, mock_user):
incomplete_data = {
"title": "Incomplete Event"
# Missing required fields like slug and event_date
}
response = self.client.post("/events/", json=incomplete_data)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_create_event_with_past_date_fails(self, db_session, mock_user, past_event_data):
response = self.client.post("/events/", json=past_event_data)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.json()
assert "Event date cannot be in the past" in response.text
def test_create_event_invalid_slug_fails(self, db_session, mock_user, invalid_slug_event_data):
response = self.client.post("/events/", json=invalid_slug_event_data)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.json()
assert "String should match pattern" in response.text
def test_create_event_unauthorized_fails(self, create_test_client, db_session, event_data):
client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=None
)
response = client.post("/events/", json=event_data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
assert "Invalid authentication credentials" in response.text
class TestGetUserEvents:
"""Tests for the get_user_events endpoint."""
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user):
self.client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
def create_mock_events(self, num_events, active=True, public=True):
"""Utility function to create mock events in the database."""
events = []
for i in range(num_events):
event_date = datetime.now(tz=timezone.utc) + timedelta(days=i + 1)
mock_event = Event(
id=uuid4(),
title=f"Mock Event {i}",
slug=f"mock-event-{uuid4().hex[:8]}",
event_date=event_date,
timezone="UTC",
is_public=public,
is_active=active,
created_by=self.mock_user.id,
created_at=datetime.now(tz=timezone.utc),
updated_at=datetime.now(tz=timezone.utc),
)
self.db_session.add(mock_event)
events.append(mock_event)
self.db_session.commit()
return events
def test_get_user_events_success(self):
"""User gets their events correctly."""
created_events = self.create_mock_events(3)
response = self.client.get("/events/me")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert "items" in data
assert len(data["items"]) == 3
returned_titles = {event["title"] for event in data["items"]}
assert all(event.title in returned_titles for event in created_events)
def test_get_user_events_pagination(self):
"""User events are returned paginated."""
self.create_mock_events(10)
response = self.client.get("/events/me?skip=5&limit=3")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert len(data["items"]) == 3
assert data["total"] == 10
def test_get_user_events_include_inactive(self):
"""Inactive events should be included if requested explicitly."""
# Create active and inactive events
self.create_mock_events(2, active=False)
self.create_mock_events(1, active=True)
response_default = self.client.get("/events/me")
data_default = response_default.json()
assert response_default.status_code == status.HTTP_200_OK
assert len(data_default["items"]) == 1 # Active included only by default
response_include_inactive = self.client.get("/events/me?include_inactive=true")
data_inactive = response_include_inactive.json()
assert response_include_inactive.status_code == status.HTTP_200_OK
assert len(data_inactive["items"]) == 3 # Inactive explicitly included
def test_get_user_events_unauthenticated_fails(self, create_test_client):
"""Endpoint must not allow access without authentication."""
unauth_client = create_test_client(
router=events_router,
prefix="/events",
db_session=self.db_session,
user=None
)
response = unauth_client.get("/events/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
assert "Invalid authentication credentials" in response.text
def test_get_user_events_no_events(self):
"""User without events should receive an empty list."""
response = self.client.get("/events/me")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert "items" in data
assert data["items"] == []
assert data["total"] == 0
class TestGetUpcomingEvents:
"""Tests for the get_upcoming_events endpoint."""
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user):
self.client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
def create_mock_event(self, days_from_now: int, is_active=True, is_public=True):
"""Utility method for creating mocked event data in database."""
event = Event(
id=uuid4(),
title=f"Upcoming Event {uuid4().hex[:4]}",
slug=f"upcoming-event-{uuid4().hex[:8]}",
description="Upcoming event description",
event_date=datetime.now(tz=timezone.utc) + timedelta(days=days_from_now),
timezone="UTC",
is_public=is_public,
is_active=is_active,
created_by=self.mock_user.id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
self.db_session.add(event)
self.db_session.commit()
return event
def test_get_upcoming_events_success_default_limit(self):
"""Ensure upcoming events are retrieved according to default limit."""
for i in range(15):
self.create_mock_event(days_from_now=i + 1)
response = self.client.get("/events/upcoming")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert "items" in data # asserting structure explicitly
assert len(data["items"]) == 15 # explicitly checking items list length
assert data["total"] == 15 # check total is correct
assert data["size"] == 100 # default size
def test_get_upcoming_events_with_query_limit(self):
"""Verifies the limit parameter works correctly."""
# create 5 events
for i in range(5):
self.create_mock_event(days_from_now=i + 1)
response = self.client.get("/events/upcoming?limit=3")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert "items" in data # asserting structure explicitly
assert len(data["items"]) == 3
assert data["total"] == 5
assert data["size"] == 3
def test_get_upcoming_events_only_active_and_future_events_returned(self):
"""Ensure only future active events are returned."""
# Past events and inactive events shouldn't be returned
self.create_mock_event(days_from_now=-5) # past event
self.create_mock_event(days_from_now=3, is_active=False) # inactive event
valid_event = self.create_mock_event(days_from_now=5) # active future event
response = self.client.get("/events/upcoming")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert len(data["items"]) == 1
assert data["items"][0]["slug"] == valid_event.slug
def test_get_upcoming_events_unauthenticated_fails(self, create_test_client, db_session):
"""Verify unauthorized users cannot access this endpoint."""
client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=None,
)
response = client.get("/events/upcoming")
assert response.status_code == status.HTTP_401_UNAUTHORIZED, response.json()
assert "Invalid authentication credentials" in response.text
def test_get_upcoming_events_no_events_available(self):
"""Check the response if no upcoming events exist."""
response = self.client.get("/events/upcoming")
assert response.status_code == status.HTTP_200_OK, response.json()
data = response.json()
assert isinstance(data["items"], list) and len(data["items"]) == 0
class TestGetPublicEvents:
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user):
self.client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
def create_mock_event(self, days_from_now: int, is_public=True, is_active=True):
event = Event(
id=uuid4(),
title=f"Event {uuid4().hex[:4]}",
slug=f"event-{uuid4().hex[:8]}",
description="Event description",
event_date=datetime.now(tz=timezone.utc) + timedelta(days=days_from_now),
timezone="UTC",
is_public=is_public,
is_active=is_active,
created_by=self.mock_user.id,
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
self.db_session.add(event)
self.db_session.commit()
return event
def test_public_events_success_default_limit(self):
"""Return default limit 100 public events"""
for i in range(120):
self.create_mock_event(days_from_now=i + 1, is_public=True)
response = self.client.get("/events/public")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["items"]) == 100 # default limit
assert data["total"] == 120
assert data["page"] == 1
assert data["size"] == 100
def test_public_events_pagination(self):
"""Return paginated events"""
for i in range(30):
self.create_mock_event(days_from_now=i + 1, is_public=True)
response = self.client.get("/events/public?skip=10&limit=5")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["items"]) == 5
assert data["total"] == 30
assert data["page"] == 3 # skip=10, limit=5 means page=3
assert data["size"] == 5
def test_non_public_events_never_returned(self):
"""Ensure events marked is_public=False aren't retrieved"""
for i in range(10):
self.create_mock_event(days_from_now=i + 1, is_public=False)
for i in range(5):
self.create_mock_event(days_from_now=i + 1, is_public=True)
response = self.client.get("/events/public")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["items"]) == 5 # only public events returned
assert data["total"] == 5
def test_public_events_no_events_available(self):
"""Edge case: when no public events in db"""
response = self.client.get("/events/public")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data["items"]) == 0
assert data["total"] == 0
assert data["page"] == 1
assert data["size"] == 100
@pytest.mark.parametrize("endpoint_type", ["id", "slug"])
# @pytest.mark.parametrize("endpoint_type", ["id"])
class TestGetEvent:
@pytest.fixture(autouse=True)
def setup_method(self, create_test_client, db_session, mock_user, endpoint_type):
self.client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=mock_user
)
self.db_session = db_session
self.mock_user = mock_user
self.endpoint_type = endpoint_type
def create_mock_user(
self,
email="testuser@example.com",
is_superuser=False,
is_active=True
):
user = User(
id=uuid.uuid4(),
email=email,
password_hash="mockhashedpassword",
first_name="Test",
last_name="User",
phone_number="1234567890",
is_active=is_active,
is_superuser=is_superuser,
preferences=None,
)
self.db_session.add(user)
self.db_session.commit()
return user
def create_mock_event(
self,
created_by: UUID,
title: str = "Test Event",
slug: str = "test-event",
description: str = "A sample event for testing purposes.",
event_date: Optional[datetime] = None,
timezone_str: str = "UTC",
is_public: bool = False,
is_active: bool = True,
managers: Optional[List] = None,
access_code: Optional[str] = None,
manager_role: EventManagerRole = EventManagerRole.ADMIN,
):
if event_date is None:
event_date = datetime.now(timezone.utc) + timedelta(
days=10) # Default to 10 days in future if not specified
# Create Event instance
mock_event = Event(
title=title,
slug=slug,
description=description,
event_date=event_date,
timezone=timezone_str,
created_by=created_by,
is_public=is_public,
is_active=is_active,
rsvp_enabled=True,
gift_registry_enabled=True,
updates_enabled=True,
access_code=access_code
)
self.db_session.add(mock_event)
self.db_session.flush() # Use flush here to get a valid event_id before assigning managers
# If managers are provided, set EventManager instances
if managers:
for manager in managers:
role_permissions = ROLE_PERMISSIONS.get(manager_role, {})
event_manager = EventManager(
user_id=manager.id,
event_id=mock_event.id,
assigned_by=created_by,
role=manager_role,
can_edit=role_permissions.get("can_edit", False),
can_invite=role_permissions.get("can_invite", False),
can_manage_gifts=role_permissions.get("can_manage_gifts", False),
can_send_updates=role_permissions.get("can_send_updates", False),
can_view_analytics=role_permissions.get("can_view_analytics", False),
assigned_at=datetime.now(timezone.utc),
)
self.db_session.add(event_manager)
# persist the changes
self.db_session.commit()
self.db_session.refresh(mock_event)
return mock_event
def get_event_endpoint(self, event_obj, access_code=None):
"""
Helper method to dynamically build the endpoint URL based on the test parameter.
"""
if self.endpoint_type == "id":
endpoint = f"/events/{event_obj.id}"
else:
endpoint = f"/events/by-slug/{event_obj.slug}"
if access_code is not None:
endpoint += f"?access_code={access_code}"
return endpoint
def test_get_event_by_creator_success(self):
mocked_event = self.create_mock_event(created_by=self.mock_user.id, is_public=True)
endpoint = self.get_event_endpoint(mocked_event)
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == str(mocked_event.id)
def test_get_event_by_manager_success(self, mock_user):
manager_user = self.create_mock_user(email="manager@example.com")
mocked_event = self.create_mock_event(created_by=self.mock_user.id, managers=[manager_user], is_public=True)
self.client.user_crud = manager_user
endpoint = self.get_event_endpoint(mocked_event)
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == str(mocked_event.id)
def test_get_event_by_superuser_success(self):
superuser = self.create_mock_user(email="superuser@example.com", is_superuser=True)
mocked_event = self.create_mock_event(created_by=self.mock_user.id, is_public=True)
self.client.user_crud = superuser
endpoint = self.get_event_endpoint(mocked_event)
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == str(mocked_event.id)
def test_get_event_not_exists(self):
random_event_id = uuid4()
endpoint = self.get_event_endpoint(SimpleNamespace(**{"id": random_event_id, "slug": "random-slug"}))
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "Event not found"
def test_get_public_event_by_non_related_user_success(self):
other_user = self.create_mock_user(email="randomuser@example.com")
mocked_event = self.create_mock_event(
created_by=self.mock_user.id,
is_public=True
)
self.client.user_crud = other_user
endpoint = self.get_event_endpoint(mocked_event)
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == str(mocked_event.id)
def test_get_private_event_by_guest_user_success(self):
guest_user = self.create_mock_user(email="guestuser@example.com")
mocked_event = self.create_mock_event(
created_by=self.mock_user.id,
is_public=False
)
guest_entry = Guest(
full_name="Guest User",
invitation_code="0000",
user_id=guest_user.id,
event_id=mocked_event.id,
invited_by=self.mock_user.id,
)
self.db_session.add(guest_entry)
self.db_session.commit()
self.client.user_crud = guest_user
endpoint = self.get_event_endpoint(mocked_event)
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == str(mocked_event.id)
def test_get_private_event_non_related_user_forbidden(self):
creator_user = self.create_mock_user(email="creator@example.com")
other_user = self.create_mock_user(email="nonrelated@example.com", is_superuser=False)
mocked_event = self.create_mock_event(
created_by=creator_user.id,
access_code="1234",
is_public=False
)
self.client.user_crud = other_user
endpoint = self.get_event_endpoint(mocked_event, access_code="123")
response = self.client.get(endpoint)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions to access this event"
def test_get_event_unauthenticated_user_fails(self, create_test_client, db_session):
mocked_event = self.create_mock_event(
created_by=self.mock_user.id,
is_public=False
)
client = create_test_client(
router=events_router,
prefix="/events",
db_session=db_session,
user=None,
)
self.client.user_crud = None # Simulate no authenticated user
endpoint = self.get_event_endpoint(mocked_event)
response = client.get(endpoint)
assert response.status_code == status.HTTP_403_FORBIDDEN
assert response.json()["detail"] == "Not enough permissions to access this event"