Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion services/event/app/models/event.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from datetime import datetime, timezone

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator

from app.models.common import PyObjectId

Expand All @@ -18,6 +18,18 @@ class EventCreate(BaseModel):
place: str = Field(min_length=1, max_length=200)
speakers: list[Speaker] = Field(default_factory=list)
image_url: str | None = None
tags: list[str] = Field(default_factory=list)
registration_form_url: str | None = None
event_type: str = Field(min_length=1, max_length=100)

@field_validator("tags")
@classmethod
def validate_tags(cls, v: list[str]) -> list[str]:
for tag in v:
if not tag or len(tag) > 50:
msg = "Each tag must be a non-empty string of at most 50 characters"
raise ValueError(msg)
return v


class EventUpdate(BaseModel):
Expand All @@ -27,6 +39,20 @@ class EventUpdate(BaseModel):
place: str | None = Field(default=None, min_length=1, max_length=200)
speakers: list[Speaker] | None = None
image_url: str | None = None
tags: list[str] = Field(default_factory=list)
registration_form_url: str | None = None
event_type: str = Field(min_length=1, max_length=100)

@field_validator("tags")
@classmethod
def validate_tags(cls, v: list[str] | None) -> list[str] | None:
if v is None:
return v
for tag in v:
if not tag or len(tag) > 50:
msg = "Each tag must be a non-empty string of at most 50 characters"
raise ValueError(msg)
return v


class EventInDB(BaseModel):
Expand All @@ -37,6 +63,9 @@ class EventInDB(BaseModel):
place: str
speakers: list[Speaker] = Field(default_factory=list)
image_url: str | None = None
tags: list[str] = Field(default_factory=list)
registration_form_url: str | None = None
event_type: str = "general"
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime | None = None

Expand All @@ -51,9 +80,14 @@ class EventResponse(BaseModel):
place: str
speakers: list[Speaker]
image_url: str | None
tags: list[str]
registration_form_url: str | None
event_type: str = Field(serialization_alias="type")
created_at: datetime
updated_at: datetime | None

model_config = {"populate_by_name": True}

@classmethod
def from_db(cls, event: EventInDB) -> "EventResponse":
return cls(
Expand All @@ -64,6 +98,9 @@ def from_db(cls, event: EventInDB) -> "EventResponse":
place=event.place,
speakers=event.speakers,
image_url=event.image_url,
tags=event.tags,
registration_form_url=event.registration_form_url,
event_type=event.event_type,
created_at=event.created_at,
updated_at=event.updated_at,
)
3 changes: 3 additions & 0 deletions services/event/app/repositories/event_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ async def create(cls, event: EventCreate) -> str:
"place": event.place,
"speakers": [speaker.model_dump() for speaker in event.speakers],
"image_url": event.image_url,
"tags": event.tags,
"registration_form_url": event.registration_form_url,
"event_type": event.event_type,
"created_at": datetime.now(timezone.utc),
"updated_at": None,
}
Expand Down
12 changes: 12 additions & 0 deletions services/event/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def sample_event_data() -> EventCreate:
place="Yaşar University",
speakers=[Speaker(name="Jane Doe", title="Engineer", company="Google")],
image_url="https://example.com/image.jpg",
tags=["devfest", "gdg"],
registration_form_url="https://forms.example.com/register",
event_type="conference",
)


Expand All @@ -96,6 +99,9 @@ def sample_event_doc() -> dict[str, Any]:
"place": "Yaşar University",
"speakers": [{"name": "Jane Doe", "title": "Engineer", "company": "Google"}],
"image_url": "https://example.com/image.jpg",
"tags": ["devfest", "gdg"],
"registration_form_url": "https://forms.example.com/register",
"event_type": "conference",
"created_at": datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
"updated_at": None,
}
Expand All @@ -113,6 +119,9 @@ def sample_event_docs() -> list[dict[str, Any]]:
"place": "Yaşar University",
"speakers": [],
"image_url": None,
"tags": [],
"registration_form_url": None,
"event_type": "conference",
"created_at": datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
"updated_at": None,
},
Expand All @@ -126,6 +135,9 @@ def sample_event_docs() -> list[dict[str, Any]]:
{"name": "John Smith", "title": "Developer", "company": "Flutter"}
],
"image_url": "https://example.com/flutter.jpg",
"tags": ["flutter", "mobile"],
"registration_form_url": "https://forms.example.com/flutter",
"event_type": "workshop",
"created_at": datetime(2025, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
"updated_at": None,
},
Expand Down
30 changes: 30 additions & 0 deletions services/event/tests/test_api_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def test_creates_event_201(self, sync_client, valid_api_token):
"description": "Annual developer festival.",
"date": "2099-11-15T10:00:00Z",
"place": "Yaşar University",
"event_type": "conference",
"speakers": [
{"name": "Jane Doe", "title": "Engineer", "company": "Google"}
],
Expand All @@ -79,6 +80,22 @@ def test_validates_missing_title_422(self, sync_client, valid_api_token):
"description": "Some event",
"date": "2099-11-15T10:00:00Z",
"place": "Somewhere",
"event_type": "meetup",
},
)

assert response.status_code == 422

def test_validates_missing_event_type_422(self, sync_client, valid_api_token):
"""POST with missing event_type returns 422."""
response = sync_client.post(
"/events/",
headers={"X-API-Token": valid_api_token},
json={
"title": "Test Event",
"description": "Some event",
"date": "2099-11-15T10:00:00Z",
"place": "Somewhere",
},
)

Expand All @@ -99,6 +116,7 @@ def test_handles_past_date_400(self, sync_client, valid_api_token):
"description": "This event is in the past.",
"date": "2020-01-01T00:00:00Z",
"place": "Nowhere",
"event_type": "meetup",
},
)

Expand All @@ -119,6 +137,9 @@ def test_returns_event_200(self, sync_client, valid_api_token):
place="Yaşar University",
speakers=[Speaker(name="Jane Doe", title="Engineer", company="Google")],
image_url="https://example.com/image.jpg",
tags=["devfest", "gdg"],
registration_form_url="https://forms.example.com/register",
event_type="conference",
created_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
updated_at=None,
)
Expand Down Expand Up @@ -167,6 +188,9 @@ def test_updates_event_200(self, sync_client, valid_api_token):
place="Yaşar University",
speakers=[],
image_url=None,
tags=[],
registration_form_url=None,
event_type="conference",
created_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2025, 1, 2, 12, 0, 0, tzinfo=timezone.utc),
)
Expand Down Expand Up @@ -247,6 +271,9 @@ def test_returns_events_200(self, sync_client, valid_api_token):
place="Yaşar University",
speakers=[],
image_url=None,
tags=[],
registration_form_url=None,
event_type="conference",
created_at=datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
updated_at=None,
),
Expand All @@ -258,6 +285,9 @@ def test_returns_events_200(self, sync_client, valid_api_token):
place="Engineering Building",
speakers=[],
image_url=None,
tags=["flutter"],
registration_form_url=None,
event_type="workshop",
created_at=datetime(2025, 2, 1, 0, 0, 0, tzinfo=timezone.utc),
updated_at=None,
),
Expand Down
45 changes: 33 additions & 12 deletions services/event/tests/test_event_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ async def test_document_structure(self, mock_mongodb, sample_event_data):
assert call_args["image_url"] == "https://example.com/image.jpg"
assert len(call_args["speakers"]) == 1
assert call_args["speakers"][0]["name"] == "Jane Doe"
assert call_args["tags"] == ["devfest", "gdg"]
assert (
call_args["registration_form_url"] == "https://forms.example.com/register"
)
assert call_args["event_type"] == "conference"

async def test_sets_timestamps(self, mock_mongodb, sample_event_data):
"""Create sets created_at and updated_at=None."""
Expand Down Expand Up @@ -88,19 +93,19 @@ async def test_returns_event_when_found(self, mock_mongodb, sample_event_doc):
assert event is not None
assert str(event.id) == "507f1f77bcf86cd799439011"
assert event.title == "GDG DevFest 2025"
assert event.tags == ["devfest", "gdg"]
assert event.registration_form_url == "https://forms.example.com/register"
assert event.event_type == "conference"

async def test_returns_none_when_not_found(self, mock_mongodb):
"""get_by_id returns None when document does not exist."""
mock_mongodb["events"].find_one.return_value = None

event = await EventRepository.get_by_id("507f1f77bcf86cd799439011")

assert event is None

async def test_returns_none_for_invalid_id(self, mock_mongodb):
"""get_by_id returns None for invalid ObjectId string."""
event = await EventRepository.get_by_id("invalid-id")

assert event is None
mock_mongodb["events"].find_one.assert_not_called()

Expand Down Expand Up @@ -171,7 +176,7 @@ async def test_empty_update_returns_current_event(
mock_mongodb["events"].update_one.assert_not_called()

async def test_empty_update_raises_not_found_for_missing(self, mock_mongodb):
"""Update with no fields raises EventNotFoundError if event doesn't exist."""
"""Update with no fields raises EventNotFoundError if missing."""
from app.models.event import EventUpdate

mock_mongodb["events"].find_one.return_value = None
Expand Down Expand Up @@ -201,32 +206,52 @@ async def test_updates_speakers(self, mock_mongodb, sample_event_doc):
assert len(result.speakers) == 1
assert result.speakers[0].name == "New Speaker"

async def test_updates_tags(self, mock_mongodb, sample_event_doc):
"""Update persists tags correctly."""
from app.models.event import EventUpdate

updated_doc = {**sample_event_doc, "tags": ["new-tag"]}
mock_mongodb["events"].update_one.return_value = MagicMock(matched_count=1)
mock_mongodb["events"].find_one.return_value = updated_doc

update = EventUpdate(tags=["new-tag"])
result = await EventRepository.update("507f1f77bcf86cd799439011", update)
assert result.tags == ["new-tag"]

async def test_updates_event_type(self, mock_mongodb, sample_event_doc):
"""Update persists event_type correctly."""
from app.models.event import EventUpdate

updated_doc = {**sample_event_doc, "event_type": "workshop"}
mock_mongodb["events"].update_one.return_value = MagicMock(matched_count=1)
mock_mongodb["events"].find_one.return_value = updated_doc

update = EventUpdate(event_type="workshop")
result = await EventRepository.update("507f1f77bcf86cd799439011", update)
assert result.event_type == "workshop"


class TestDelete:
"""Tests for EventRepository.delete()."""

async def test_deletes_existing_event(self, mock_mongodb):
"""Delete removes the document when it exists."""
mock_mongodb["events"].delete_one.return_value = MagicMock(deleted_count=1)

await EventRepository.delete("507f1f77bcf86cd799439011")

mock_mongodb["events"].delete_one.assert_called_once_with(
{"_id": ObjectId("507f1f77bcf86cd799439011")}
)

async def test_raises_not_found_when_deleted_count_zero(self, mock_mongodb):
"""Delete raises EventNotFoundError when deleted_count is 0."""
mock_mongodb["events"].delete_one.return_value = MagicMock(deleted_count=0)

with pytest.raises(EventNotFoundError):
await EventRepository.delete("507f1f77bcf86cd799439011")

async def test_raises_not_found_for_invalid_id(self, mock_mongodb):
"""Delete raises EventNotFoundError for invalid ObjectId."""
with pytest.raises(EventNotFoundError):
await EventRepository.delete("invalid-id")

mock_mongodb["events"].delete_one.assert_not_called()


Expand All @@ -238,19 +263,15 @@ async def test_returns_events_list(self, mock_mongodb, sample_event_docs):
mock_mongodb["events"].find.return_value = create_async_cursor(
sample_event_docs
)

events = await EventRepository.list_events()

assert len(events) == 2
assert events[0].title == "GDG DevFest 2025"
assert events[1].title == "Flutter Workshop"

async def test_returns_empty_list(self, mock_mongodb):
"""list_events returns empty list when no events exist."""
mock_mongodb["events"].find.return_value = create_async_cursor([])

events = await EventRepository.list_events()

assert events == []

async def test_applies_sort_skip_limit(self, mock_mongodb):
Expand Down
8 changes: 8 additions & 0 deletions services/event/tests/test_event_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ def _make_db_event(**kwargs) -> EventInDB:
"place": "Yaşar University",
"speakers": [],
"image_url": None,
"tags": ["devfest"],
"registration_form_url": None,
"event_type": "conference",
"created_at": datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
"updated_at": None,
}
Expand All @@ -43,6 +46,7 @@ async def test_creates_event_with_future_date(self, mock_mongodb):
description="Annual developer festival.",
date=datetime(2099, 11, 15, 10, 0, 0, tzinfo=timezone.utc),
place="Yaşar University",
event_type="conference",
)
event_id = await EventService.create_event(data)

Expand All @@ -55,6 +59,7 @@ async def test_raises_value_error_for_past_date(self, mock_mongodb):
description="Already happened.",
date=datetime(2020, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
place="Somewhere",
event_type="meetup",
)

with pytest.raises(ValueError, match="Event date must be in the future"):
Expand All @@ -72,6 +77,7 @@ async def test_delegates_to_repository(self, mock_mongodb):
description="A workshop.",
date=datetime(2099, 6, 1, 10, 0, 0, tzinfo=timezone.utc),
place="Lab",
event_type="workshop",
)
await EventService.create_event(data)

Expand All @@ -95,6 +101,7 @@ async def test_returns_event_response(self, mock_mongodb):
assert response is not None
assert response.id == "507f1f77bcf86cd799439011"
assert response.title == "GDG DevFest 2025"
assert response.event_type == "conference"

async def test_returns_none_when_not_found(self, mock_mongodb):
"""get_event_by_id returns None when event doesn't exist."""
Expand Down Expand Up @@ -174,6 +181,7 @@ async def test_returns_list_of_event_responses(self, mock_mongodb):
_make_db_event(
_id=ObjectId("507f1f77bcf86cd799439012"),
title="Flutter Workshop",
event_type="workshop",
),
]

Expand Down
Loading