diff --git a/backend/apps/api/rest/v0/chapter.py b/backend/apps/api/rest/v0/chapter.py index 35b4981996..4e56ddd98c 100644 --- a/backend/apps/api/rest/v0/chapter.py +++ b/backend/apps/api/rest/v0/chapter.py @@ -91,7 +91,9 @@ def get_chapter( """Get chapter.""" if chapter := ChapterModel.active_chapters.filter( key__iexact=( - chapter_id if chapter_id.startswith("www-chapter-") else f"www-chapter-{chapter_id}" + chapter_id + if chapter_id.lower().startswith("www-chapter-") + else f"www-chapter-{chapter_id}" ) ).first(): return chapter diff --git a/backend/apps/api/rest/v0/committee.py b/backend/apps/api/rest/v0/committee.py index 2171b74587..0888673847 100644 --- a/backend/apps/api/rest/v0/committee.py +++ b/backend/apps/api/rest/v0/committee.py @@ -76,16 +76,16 @@ def list_committees( summary="Get committee", ) @decorate_view(cache_response()) -def get_chapter( +def get_committee( request: HttpRequest, committee_id: str = Path(example="project"), ) -> CommitteeDetail | CommitteeError: - """Get chapter.""" + """Get committee.""" if committee := CommitteeModel.active_committees.filter( is_active=True, key__iexact=( committee_id - if committee_id.startswith("www-committee-") + if committee_id.lower().startswith("www-committee-") else f"www-committee-{committee_id}" ), ).first(): diff --git a/backend/apps/api/rest/v0/event.py b/backend/apps/api/rest/v0/event.py index 51b8a39240..d296050768 100644 --- a/backend/apps/api/rest/v0/event.py +++ b/backend/apps/api/rest/v0/event.py @@ -58,7 +58,10 @@ def list_events( ), ) -> list[Event]: """Get all events.""" - return EventModel.objects.order_by(ordering or "-start_date", "-end_date") + if ordering and ordering.lstrip("-") == "end_date": + secondary = "-start_date" if ordering.startswith("-") else "start_date" + return EventModel.objects.order_by(ordering, secondary, "id") + return EventModel.objects.order_by(ordering or "-start_date", "-end_date", "id") @router.get( diff --git a/backend/apps/api/rest/v0/issue.py b/backend/apps/api/rest/v0/issue.py index a249b2f039..909ba98c68 100644 --- a/backend/apps/api/rest/v0/issue.py +++ b/backend/apps/api/rest/v0/issue.py @@ -89,7 +89,9 @@ def list_issues( if filters.state: issues = issues.filter(state=filters.state) - return issues.order_by(ordering or "-created_at", "-updated_at") + if ordering: + return issues.order_by(ordering, "id") + return issues.order_by("-created_at", "-updated_at", "id") @router.get( diff --git a/backend/apps/api/rest/v0/milestone.py b/backend/apps/api/rest/v0/milestone.py index 17ec54c197..a9d08afd73 100644 --- a/backend/apps/api/rest/v0/milestone.py +++ b/backend/apps/api/rest/v0/milestone.py @@ -77,7 +77,10 @@ class MilestoneFilter(FilterSchema): def list_milestones( request: HttpRequest, filters: MilestoneFilter = Query(...), - ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = None, + ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query( + None, + description="Ordering field", + ), ) -> list[Milestone]: """Get all milestones.""" milestones = MilestoneModel.objects.select_related("repository", "repository__organization") @@ -91,7 +94,9 @@ def list_milestones( if filters.state: milestones = milestones.filter(state=filters.state) - return milestones.order_by(ordering or "-created_at", "-updated_at") + if ordering: + return milestones.order_by(ordering, "id") + return milestones.order_by("-created_at", "-updated_at", "id") @router.get( diff --git a/backend/apps/api/rest/v0/project.py b/backend/apps/api/rest/v0/project.py index f2f9bd920d..fa2ff17e46 100644 --- a/backend/apps/api/rest/v0/project.py +++ b/backend/apps/api/rest/v0/project.py @@ -99,7 +99,9 @@ def get_project( """Get project.""" if project := ProjectModel.active_projects.filter( key__iexact=( - project_id if project_id.startswith("www-project-") else f"www-project-{project_id}" + project_id + if project_id.lower().startswith("www-project-") + else f"www-project-{project_id}" ) ).first(): return project diff --git a/backend/apps/api/rest/v0/release.py b/backend/apps/api/rest/v0/release.py index 2d4c97d0bb..0867e2d5bc 100644 --- a/backend/apps/api/rest/v0/release.py +++ b/backend/apps/api/rest/v0/release.py @@ -90,7 +90,9 @@ def list_release( if filters.tag_name: releases = releases.filter(tag_name=filters.tag_name) - return releases.order_by(ordering or "-published_at", "-created_at") + if ordering: + return releases.order_by(ordering, "id") + return releases.order_by("-published_at", "-created_at", "id") @router.get( diff --git a/backend/apps/api/rest/v0/repository.py b/backend/apps/api/rest/v0/repository.py index 2b0588f83e..6064c0691b 100644 --- a/backend/apps/api/rest/v0/repository.py +++ b/backend/apps/api/rest/v0/repository.py @@ -72,7 +72,9 @@ def list_repository( if filters.organization_id: repositories = repositories.filter(organization__login__iexact=filters.organization_id) - return repositories.order_by(ordering or "-created_at", "-updated_at") + if ordering: + return repositories.order_by(ordering, "id") + return repositories.order_by("-created_at", "-updated_at", "id") @router.get( diff --git a/backend/tests/apps/api/decorators/cache_test.py b/backend/tests/apps/api/decorators/cache_test.py index 9586217bae..897326b62a 100644 --- a/backend/tests/apps/api/decorators/cache_test.py +++ b/backend/tests/apps/api/decorators/cache_test.py @@ -70,7 +70,7 @@ def test_get_request_returns_cached_response(self, mock_cache, mock_request): mock_cache.set.assert_not_called() view_func.assert_not_called() - @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE"]) + @pytest.mark.parametrize("method", ["POST", "PUT", "DELETE", "PATCH"]) @patch("apps.api.decorators.cache.cache") def test_non_get_head_requests_not_cached(self, mock_cache, method, mock_request): """Test that non-GET/HEAD requests are not cached.""" @@ -106,3 +106,85 @@ def test_non_200_responses_not_cached(self, mock_cache, status_code, mock_reques mock_cache.get.assert_called_once() mock_cache.set.assert_not_called() view_func.assert_called_once_with(mock_request) + + @patch("apps.api.decorators.cache.settings") + @patch("apps.api.decorators.cache.cache") + def test_default_ttl_from_settings(self, mock_cache, mock_settings, mock_request): + """Test that default TTL is used from settings when not specified.""" + mock_settings.API_CACHE_TIME_SECONDS = 3600 + mock_settings.API_CACHE_PREFIX = "test-prefix" + mock_cache.get.return_value = None + view_func = MagicMock(return_value=HttpResponse(status=HTTPStatus.OK)) + decorated_view = cache_response()(view_func) + + response = decorated_view(mock_request) + + assert response.status_code == HTTPStatus.OK + mock_cache.set.assert_called_once() + call_args = mock_cache.set.call_args + assert call_args[1]["timeout"] == 3600 + view_func.assert_called_once_with(mock_request) + + @patch("apps.api.decorators.cache.settings") + @patch("apps.api.decorators.cache.cache") + def test_default_prefix_from_settings(self, mock_cache, mock_settings, mock_request): + """Test that default prefix is used from settings when not specified.""" + mock_settings.API_CACHE_TIME_SECONDS = 60 + mock_settings.API_CACHE_PREFIX = "custom-api-prefix" + mock_cache.get.return_value = None + view_func = MagicMock(return_value=HttpResponse(status=HTTPStatus.OK)) + decorated_view = cache_response()(view_func) + + response = decorated_view(mock_request) + + assert response.status_code == HTTPStatus.OK + mock_cache.get.assert_called_once() + cache_key = mock_cache.get.call_args[0][0] + assert cache_key == "custom-api-prefix:/api/test" + view_func.assert_called_once_with(mock_request) + + @patch("apps.api.decorators.cache.cache") + def test_head_request_caches_response(self, mock_cache, mock_request): + """Test that HEAD requests are cached like GET requests.""" + mock_request.method = "HEAD" + mock_cache.get.return_value = None + view_func = MagicMock(return_value=HttpResponse(status=HTTPStatus.OK)) + decorated_view = cache_response(ttl=60)(view_func) + + response = decorated_view(mock_request) + + assert response.status_code == HTTPStatus.OK + mock_cache.get.assert_called_once() + mock_cache.set.assert_called_once() + view_func.assert_called_once_with(mock_request) + + @patch("apps.api.decorators.cache.cache") + def test_head_request_returns_cached_response(self, mock_cache, mock_request): + """Test that HEAD requests return cached responses.""" + mock_request.method = "HEAD" + cached_response = HttpResponse(status=HTTPStatus.OK, content=b"cached") + mock_cache.get.return_value = cached_response + view_func = MagicMock() + decorated_view = cache_response(ttl=60)(view_func) + + response = decorated_view(mock_request) + + assert response == cached_response + mock_cache.get.assert_called_once() + mock_cache.set.assert_not_called() + view_func.assert_not_called() + + @patch("apps.api.decorators.cache.cache") + def test_custom_prefix_parameter(self, mock_cache, mock_request): + """Test that custom prefix parameter is used correctly.""" + mock_cache.get.return_value = None + view_func = MagicMock(return_value=HttpResponse(status=HTTPStatus.OK)) + decorated_view = cache_response(ttl=60, prefix="my-custom-prefix")(view_func) + + response = decorated_view(mock_request) + + assert response.status_code == HTTPStatus.OK + mock_cache.get.assert_called_once() + cache_key = mock_cache.get.call_args[0][0] + assert cache_key == "my-custom-prefix:/api/test" + view_func.assert_called_once_with(mock_request) diff --git a/backend/tests/apps/api/internal/mutations/api_key_test.py b/backend/tests/apps/api/internal/mutations/api_key_test.py index ac522df823..eb6ad7d600 100644 --- a/backend/tests/apps/api/internal/mutations/api_key_test.py +++ b/backend/tests/apps/api/internal/mutations/api_key_test.py @@ -33,6 +33,67 @@ def api_key_mutations(self) -> ApiKeyMutations: """Pytest fixture to return an instance of the mutation class.""" return ApiKeyMutations() + def test_create_api_key_empty_name(self, api_key_mutations): + """Test creating an API key with an empty name.""" + info = mock_info() + name = "" + expires_at = timezone.now() + timedelta(days=30) + + result = api_key_mutations.create_api_key(info, name=name, expires_at=expires_at) + + assert isinstance(result, CreateApiKeyResult) + assert not result.ok + assert result.code == "INVALID_NAME" + assert result.message == "Name is required" + assert result.api_key is None + assert result.raw_key is None + + def test_create_api_key_whitespace_name(self, api_key_mutations): + """Test creating an API key with only whitespace in the name.""" + info = mock_info() + name = " " + expires_at = timezone.now() + timedelta(days=30) + + result = api_key_mutations.create_api_key(info, name=name, expires_at=expires_at) + + assert isinstance(result, CreateApiKeyResult) + assert not result.ok + assert result.code == "INVALID_NAME" + assert result.message == "Name is required" + assert result.api_key is None + assert result.raw_key is None + + @patch("apps.api.internal.mutations.api_key.MAX_WORD_LENGTH", 10) + def test_create_api_key_name_too_long(self, api_key_mutations): + """Test creating an API key with a name exceeding the maximum length.""" + info = mock_info() + name = "a" * 11 + expires_at = timezone.now() + timedelta(days=30) + + result = api_key_mutations.create_api_key(info, name=name, expires_at=expires_at) + + assert isinstance(result, CreateApiKeyResult) + assert not result.ok + assert result.code == "INVALID_NAME" + assert result.message == "Name too long" + assert result.api_key is None + assert result.raw_key is None + + def test_create_api_key_expires_in_past(self, api_key_mutations): + """Test creating an API key with an expiry date in the past.""" + info = mock_info() + name = "My Key" + expires_at = timezone.now() - timedelta(days=1) + + result = api_key_mutations.create_api_key(info, name=name, expires_at=expires_at) + + assert isinstance(result, CreateApiKeyResult) + assert not result.ok + assert result.code == "INVALID_DATE" + assert result.message == "Expiry date must be in future" + assert result.api_key is None + assert result.raw_key is None + @patch("apps.api.internal.mutations.api_key.ApiKey.create") def test_create_api_key_success(self, mock_api_key_create, api_key_mutations): """Test the successful creation of an API key.""" @@ -82,12 +143,13 @@ def test_create_api_key_integrity_error( ): """Test the mutation's behavior when an IntegrityError is raised.""" info = mock_info() + user = info.context.request.user name = "A key that causes a DB error" expires_at = timezone.now() + timedelta(days=30) result = api_key_mutations.create_api_key(info, name=name, expires_at=expires_at) - mock_api_key_create.assert_called_once() + mock_api_key_create.assert_called_once_with(user=user, name=name, expires_at=expires_at) mock_logger.warning.assert_called_once() assert isinstance(result, CreateApiKeyResult) diff --git a/backend/tests/apps/api/rest/v0/chapter_test.py b/backend/tests/apps/api/rest/v0/chapter_test.py index b1757cd7c6..fc9ea2b1fe 100644 --- a/backend/tests/apps/api/rest/v0/chapter_test.py +++ b/backend/tests/apps/api/rest/v0/chapter_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.chapter import ChapterDetail +from apps.api.rest.v0.chapter import ChapterDetail, get_chapter, list_chapters @pytest.mark.parametrize( @@ -41,3 +43,110 @@ def __init__(self, data): assert chapter.name == chapter_data["name"] assert chapter.region == chapter_data["region"] assert chapter.updated_at == datetime.fromisoformat(chapter_data["updated_at"]) + + +class TestListChapters: + """Test cases for list_chapters endpoint.""" + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_list_chapters_with_ordering(self, mock_active_chapters): + """Test listing chapters with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filtered_queryset = MagicMock() + mock_ordered_queryset = MagicMock() + + mock_active_chapters.order_by.return_value = mock_ordered_queryset + mock_filters.filter.return_value = mock_filtered_queryset + + result = list_chapters(mock_request, filters=mock_filters, ordering="created_at") + + mock_active_chapters.order_by.assert_called_once_with("created_at") + mock_filters.filter.assert_called_once_with(mock_ordered_queryset) + assert result == mock_filtered_queryset + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_list_chapters_with_default_ordering(self, mock_active_chapters): + """Test that None ordering triggers default '-created_at' ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filtered_queryset = MagicMock() + mock_ordered_queryset = MagicMock() + + mock_active_chapters.order_by.return_value = mock_ordered_queryset + mock_filters.filter.return_value = mock_filtered_queryset + + result = list_chapters(mock_request, filters=mock_filters, ordering=None) + + mock_active_chapters.order_by.assert_called_once_with("-created_at") + mock_filters.filter.assert_called_once_with(mock_ordered_queryset) + assert result == mock_filtered_queryset + + +class TestGetChapter: + """Test cases for get_chapter endpoint.""" + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_get_chapter_with_prefix(self, mock_active_chapters): + """Test getting a chapter when chapter_id already has www-chapter- prefix.""" + mock_request = MagicMock() + mock_chapter = MagicMock() + mock_filter = MagicMock() + + mock_active_chapters.filter.return_value = mock_filter + mock_filter.first.return_value = mock_chapter + + result = get_chapter(mock_request, chapter_id="www-chapter-london") + + mock_active_chapters.filter.assert_called_once_with(key__iexact="www-chapter-london") + mock_filter.first.assert_called_once() + assert result == mock_chapter + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_get_chapter_without_prefix(self, mock_active_chapters): + """Test getting a chapter when chapter_id needs www-chapter- prefix added.""" + mock_request = MagicMock() + mock_chapter = MagicMock() + mock_filter = MagicMock() + + mock_active_chapters.filter.return_value = mock_filter + mock_filter.first.return_value = mock_chapter + + result = get_chapter(mock_request, chapter_id="london") + + mock_active_chapters.filter.assert_called_once_with(key__iexact="www-chapter-london") + mock_filter.first.assert_called_once() + assert result == mock_chapter + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_get_chapter_not_found(self, mock_active_chapters): + """Test getting a chapter that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_active_chapters.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_chapter(mock_request, chapter_id="nonexistent") + + mock_active_chapters.filter.assert_called_once_with(key__iexact="www-chapter-nonexistent") + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Chapter not found" in result.content + + @patch("apps.api.rest.v0.chapter.ChapterModel.active_chapters") + def test_get_chapter_uppercase_prefix(self, mock_active_chapters): + """Test that uppercase prefix is detected case-insensitively.""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_chapter = MagicMock() + + mock_active_chapters.filter.return_value = mock_filter + mock_filter.first.return_value = mock_chapter + + result = get_chapter(mock_request, chapter_id="WWW-CHAPTER-London") + + # Prefix should be detected case-insensitively, no double prefix + mock_active_chapters.filter.assert_called_once_with(key__iexact="WWW-CHAPTER-London") + mock_filter.first.assert_called_once() + assert result == mock_chapter diff --git a/backend/tests/apps/api/rest/v0/committee_test.py b/backend/tests/apps/api/rest/v0/committee_test.py index 22ef0e910b..d499d56def 100644 --- a/backend/tests/apps/api/rest/v0/committee_test.py +++ b/backend/tests/apps/api/rest/v0/committee_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.committee import CommitteeDetail +from apps.api.rest.v0.committee import CommitteeDetail, get_committee, list_committees @pytest.mark.parametrize( @@ -38,3 +40,109 @@ def __init__(self, data): assert committee.key == committee_data["key"] assert committee.name == committee_data["name"] assert committee.updated_at == datetime.fromisoformat(committee_data["updated_at"]) + + +class TestListCommittees: + """Test cases for list_committees endpoint.""" + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_list_committees_with_ordering(self, mock_active_committees): + """Test listing committees with custom ordering.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_active_committees.order_by.return_value = mock_queryset + + result = list_committees(mock_request, ordering="created_at") + + mock_active_committees.order_by.assert_called_once_with("created_at") + assert result == mock_queryset + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_list_committees_with_default_ordering(self, mock_active_committees): + """Test that None ordering triggers default '-created_at' ordering.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_active_committees.order_by.return_value = mock_queryset + + result = list_committees(mock_request, ordering=None) + + mock_active_committees.order_by.assert_called_once_with("-created_at") + assert result == mock_queryset + + +class TestGetCommittee: + """Test cases for get_committee endpoint.""" + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_get_committee_with_prefix(self, mock_active_committees): + """Test getting a committee when committee_id already has www-committee- prefix.""" + mock_request = MagicMock() + mock_committee = MagicMock() + mock_filter = MagicMock() + + mock_active_committees.filter.return_value = mock_filter + mock_filter.first.return_value = mock_committee + + result = get_committee(mock_request, committee_id="www-committee-project") + + mock_active_committees.filter.assert_called_once_with( + is_active=True, key__iexact="www-committee-project" + ) + mock_filter.first.assert_called_once() + assert result == mock_committee + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_get_committee_without_prefix(self, mock_active_committees): + """Test getting a committee when committee_id needs www-committee- prefix added.""" + mock_request = MagicMock() + mock_committee = MagicMock() + mock_filter = MagicMock() + + mock_active_committees.filter.return_value = mock_filter + mock_filter.first.return_value = mock_committee + + result = get_committee(mock_request, committee_id="project") + + mock_active_committees.filter.assert_called_once_with( + is_active=True, key__iexact="www-committee-project" + ) + mock_filter.first.assert_called_once() + assert result == mock_committee + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_get_committee_not_found(self, mock_active_committees): + """Test getting a committee that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_active_committees.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_committee(mock_request, committee_id="nonexistent") + + mock_active_committees.filter.assert_called_once_with( + is_active=True, key__iexact="www-committee-nonexistent" + ) + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Committee not found" in result.content + + @patch("apps.api.rest.v0.committee.CommitteeModel.active_committees") + def test_get_committee_uppercase_prefix(self, mock_active_committees): + """Test that uppercase prefix is detected case-insensitively.""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_committee = MagicMock() + + mock_active_committees.filter.return_value = mock_filter + mock_filter.first.return_value = mock_committee + + result = get_committee(mock_request, committee_id="WWW-COMMITTEE-Project") + + mock_active_committees.filter.assert_called_once_with( + is_active=True, key__iexact="WWW-COMMITTEE-Project" + ) + mock_filter.first.assert_called_once() + assert result == mock_committee diff --git a/backend/tests/apps/api/rest/v0/event_test.py b/backend/tests/apps/api/rest/v0/event_test.py index d41b647aa7..4a61fdb057 100644 --- a/backend/tests/apps/api/rest/v0/event_test.py +++ b/backend/tests/apps/api/rest/v0/event_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.event import EventDetail +from apps.api.rest.v0.event import EventDetail, get_event, list_events @pytest.mark.parametrize( @@ -35,3 +37,95 @@ def test_event_serializer_validation(event_data): assert event.name == event_data["name"] assert event.start_date == datetime.fromisoformat(event_data["start_date"]) assert event.url == event_data["url"] + + +class TestListEvents: + """Test cases for list_events endpoint.""" + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_list_events_with_ordering(self, mock_objects): + """Test listing events with custom ordering.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_objects.order_by.return_value = mock_queryset + + result = list_events(mock_request, ordering="start_date") + + mock_objects.order_by.assert_called_once_with("start_date", "-end_date", "id") + assert result == mock_queryset + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_list_events_with_default_ordering(self, mock_objects): + """Test that None ordering triggers default '-start_date' ordering.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_objects.order_by.return_value = mock_queryset + + result = list_events(mock_request, ordering=None) + + mock_objects.order_by.assert_called_once_with("-start_date", "-end_date", "id") + assert result == mock_queryset + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_list_events_ordering_by_end_date(self, mock_objects): + """Test listing events with ordering by end_date uses start_date as secondary.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_objects.order_by.return_value = mock_queryset + + result = list_events(mock_request, ordering="end_date") + + mock_objects.order_by.assert_called_once_with("end_date", "start_date", "id") + assert result == mock_queryset + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_list_events_ordering_by_negative_end_date(self, mock_objects): + """Test listing events with ordering by -end_date uses -start_date as secondary.""" + mock_request = MagicMock() + mock_queryset = MagicMock() + + mock_objects.order_by.return_value = mock_queryset + + result = list_events(mock_request, ordering="-end_date") + + mock_objects.order_by.assert_called_once_with("-end_date", "-start_date", "id") + assert result == mock_queryset + + +class TestGetEvent: + """Test cases for get_event endpoint.""" + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_get_event_found(self, mock_objects): + """Test getting an event that exists.""" + mock_request = MagicMock() + mock_event = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_event + + result = get_event(mock_request, event_id="owasp-global-appsec-usa-2025") + + mock_objects.filter.assert_called_once_with(key__iexact="owasp-global-appsec-usa-2025") + mock_filter.first.assert_called_once() + assert result == mock_event + + @patch("apps.api.rest.v0.event.EventModel.objects") + def test_get_event_not_found(self, mock_objects): + """Test getting an event that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_event(mock_request, event_id="nonexistent-event") + + mock_objects.filter.assert_called_once_with(key__iexact="nonexistent-event") + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Event not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/issue_test.py b/backend/tests/apps/api/rest/v0/issue_test.py index 3211e4506e..04473b06cb 100644 --- a/backend/tests/apps/api/rest/v0/issue_test.py +++ b/backend/tests/apps/api/rest/v0/issue_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.issue import IssueDetail +from apps.api.rest.v0.issue import IssueDetail, get_issue, list_issues class TestIssueSchema: @@ -36,3 +38,177 @@ def test_issue_schema(self, issue_data): assert issue.title == issue_data["title"] assert issue.updated_at == datetime.fromisoformat(issue_data["updated_at"]) assert issue.url == issue_data["url"] + + +class TestListIssues: + """Test cases for list_issues endpoint.""" + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_list_issues_with_organization_filter(self, mock_objects): + """Test listing issues filtered by organization.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = "OWASP" + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_filtered = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filtered + mock_filtered.order_by.return_value = mock_ordered + + result = list_issues(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.filter.assert_called_once_with( + repository__organization__login__iexact="OWASP" + ) + mock_filtered.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_list_issues_with_repository_filter(self, mock_objects): + """Test listing issues filtered by repository.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = "Nest" + mock_filters.state = None + + mock_select_related = MagicMock() + mock_filtered = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filtered + mock_filtered.order_by.return_value = mock_ordered + + result = list_issues(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.filter.assert_called_once_with(repository__name__iexact="Nest") + mock_filtered.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_list_issues_with_state_filter(self, mock_objects): + """Test listing issues filtered by state.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = "open" + + mock_select_related = MagicMock() + mock_filtered = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filtered + mock_filtered.order_by.return_value = mock_ordered + + result = list_issues(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.filter.assert_called_once_with(state="open") + mock_filtered.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_list_issues_with_custom_ordering(self, mock_objects): + """Test listing issues with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.order_by.return_value = mock_ordered + + result = list_issues(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.order_by.assert_called_once_with("created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_list_issues_ordering_by_updated_at(self, mock_objects): + """Test listing issues with ordering by updated_at avoids double ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.order_by.return_value = mock_ordered + + result = list_issues(mock_request, filters=mock_filters, ordering="-updated_at") + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.order_by.assert_called_once_with("-updated_at", "id") + assert result == mock_ordered + + +class TestGetIssue: + """Test cases for get_issue endpoint.""" + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_get_issue_found(self, mock_objects): + """Test getting an issue that exists.""" + mock_request = MagicMock() + mock_issue = MagicMock() + + mock_objects.get.return_value = mock_issue + + result = get_issue( + mock_request, organization_id="OWASP", repository_id="Nest", issue_id=1234 + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="Nest", + number=1234, + ) + assert result == mock_issue + + @patch("apps.api.rest.v0.issue.IssueModel.objects") + def test_get_issue_not_found(self, mock_objects): + """Test getting an issue that does not exist returns 404.""" + from apps.github.models.issue import Issue as IssueModel + + mock_request = MagicMock() + + mock_objects.get.side_effect = IssueModel.DoesNotExist + + result = get_issue( + mock_request, organization_id="OWASP", repository_id="Nest", issue_id=9999 + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="Nest", + number=9999, + ) + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Issue not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/label_test.py b/backend/tests/apps/api/rest/v0/label_test.py index 037f36de40..cef049d102 100644 --- a/backend/tests/apps/api/rest/v0/label_test.py +++ b/backend/tests/apps/api/rest/v0/label_test.py @@ -1,6 +1,8 @@ +from unittest.mock import MagicMock, patch + import pytest -from apps.api.rest.v0.label import LabelDetail +from apps.api.rest.v0.label import LabelDetail, list_label class TestLabelSchema: @@ -25,3 +27,45 @@ def test_label_schema(self, label_data): assert label.color == label_data["color"] assert label.description == label_data["description"] assert label.name == label_data["name"] + + +class TestListLabel: + """Test cases for list_label endpoint.""" + + @patch("apps.api.rest.v0.label.LabelModel.objects") + def test_list_label_with_ordering(self, mock_objects): + """Test listing labels with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_all = MagicMock() + mock_filtered = MagicMock() + mock_ordered = MagicMock() + + mock_objects.all.return_value = mock_all + mock_filters.filter.return_value = mock_filtered + mock_filtered.order_by.return_value = mock_ordered + + result = list_label(mock_request, filters=mock_filters, ordering="nest_created_at") + + mock_objects.all.assert_called_once() + mock_filters.filter.assert_called_once_with(mock_all) + mock_filtered.order_by.assert_called_once_with("nest_created_at") + assert result == mock_ordered + + @patch("apps.api.rest.v0.label.LabelModel.objects") + def test_list_label_without_ordering(self, mock_objects): + """Test listing labels without ordering (ordering=None).""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_all = MagicMock() + mock_filtered = MagicMock() + + mock_objects.all.return_value = mock_all + mock_filters.filter.return_value = mock_filtered + + result = list_label(mock_request, filters=mock_filters, ordering=None) + + mock_objects.all.assert_called_once() + mock_filters.filter.assert_called_once_with(mock_all) + mock_filtered.order_by.assert_not_called() + assert result == mock_filtered diff --git a/backend/tests/apps/api/rest/v0/member_test.py b/backend/tests/apps/api/rest/v0/member_test.py index 8444702118..84885cf758 100644 --- a/backend/tests/apps/api/rest/v0/member_test.py +++ b/backend/tests/apps/api/rest/v0/member_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.member import MemberDetail +from apps.api.rest.v0.member import MemberDetail, get_member, list_members class TestMemberSchema: @@ -47,3 +49,77 @@ def test_user_schema(self, member_data): assert member.url == member_data["url"] assert not hasattr(member, "email") + + +class TestListMembers: + """Test cases for list_members endpoint.""" + + @patch("apps.api.rest.v0.member.UserModel.objects") + def test_list_members_with_ordering(self, mock_objects): + """Test listing members with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_filtered = MagicMock() + + mock_objects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_filtered + + result = list_members(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.order_by.assert_called_once_with("created_at") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_filtered + + @patch("apps.api.rest.v0.member.UserModel.objects") + def test_list_members_with_default_ordering(self, mock_objects): + """Test that None ordering triggers default '-created_at' ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_filtered = MagicMock() + + mock_objects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_filtered + + result = list_members(mock_request, filters=mock_filters, ordering=None) + + mock_objects.order_by.assert_called_once_with("-created_at") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_filtered + + +class TestGetMember: + """Test cases for get_member endpoint.""" + + @patch("apps.api.rest.v0.member.UserModel.objects") + def test_get_member_found(self, mock_objects): + """Test getting a member that exists.""" + mock_request = MagicMock() + mock_user = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_user + + result = get_member(mock_request, member_id="johndoe") + + mock_objects.filter.assert_called_once_with(login__iexact="johndoe") + mock_filter.first.assert_called_once() + assert result == mock_user + + @patch("apps.api.rest.v0.member.UserModel.objects") + def test_get_member_not_found(self, mock_objects): + """Test getting a member that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_member(mock_request, member_id="nonexistent") + + mock_objects.filter.assert_called_once_with(login__iexact="nonexistent") + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Member not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/milestone_test.py b/backend/tests/apps/api/rest/v0/milestone_test.py index 30819756b1..29f297fb74 100644 --- a/backend/tests/apps/api/rest/v0/milestone_test.py +++ b/backend/tests/apps/api/rest/v0/milestone_test.py @@ -1,8 +1,11 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.milestone import MilestoneDetail +from apps.api.rest.v0.milestone import MilestoneDetail, get_milestone, list_milestones +from apps.github.models.milestone import Milestone as MilestoneModel class TestMilestoneSchema: @@ -87,3 +90,172 @@ def test_milestone_schema_with_minimal_data(self): assert milestone.open_issues_count == 0 assert milestone.title == "Test Milestone" assert milestone.state == "open" + + +class TestListMilestones: + """Test cases for list_milestones endpoint.""" + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_list_milestones_with_custom_ordering(self, mock_objects): + """Test listing milestones with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.order_by.return_value = mock_ordered + + result = list_milestones(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.order_by.assert_called_once_with("created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_list_milestones_ordering_by_updated_at(self, mock_objects): + """Test listing milestones with ordering by updated_at avoids double ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.order_by.return_value = mock_ordered + + result = list_milestones(mock_request, filters=mock_filters, ordering="-updated_at") + + mock_objects.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select_related.order_by.assert_called_once_with("-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_list_milestones_with_organization_filter(self, mock_objects): + """Test listing milestones with only organization filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = "OWASP" + mock_filters.repository = None + mock_filters.state = None + + mock_select_related = MagicMock() + mock_filter_org = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filter_org + mock_filter_org.order_by.return_value = mock_ordered + + result = list_milestones(mock_request, filters=mock_filters, ordering=None) + + mock_select_related.filter.assert_called_once_with( + repository__organization__login__iexact="OWASP" + ) + mock_filter_org.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_list_milestones_with_repository_filter(self, mock_objects): + """Test listing milestones with only repository filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = "Nest" + mock_filters.state = None + + mock_select_related = MagicMock() + mock_filter_repo = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filter_repo + mock_filter_repo.order_by.return_value = mock_ordered + + result = list_milestones(mock_request, filters=mock_filters, ordering=None) + + mock_select_related.filter.assert_called_once_with(repository__name__iexact="Nest") + mock_filter_repo.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_list_milestones_with_state_filter(self, mock_objects): + """Test listing milestones with only state filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.state = "closed" + + mock_select_related = MagicMock() + mock_filter_state = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select_related + mock_select_related.filter.return_value = mock_filter_state + mock_filter_state.order_by.return_value = mock_ordered + + result = list_milestones(mock_request, filters=mock_filters, ordering=None) + + mock_select_related.filter.assert_called_once_with(state="closed") + mock_filter_state.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + +class TestGetMilestone: + """Test cases for get_milestone endpoint.""" + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_get_milestone_found(self, mock_objects): + """Test getting a milestone that exists.""" + mock_request = MagicMock() + mock_milestone = MagicMock() + + mock_objects.get.return_value = mock_milestone + + result = get_milestone( + mock_request, + organization_id="OWASP", + repository_id="Nest", + milestone_id=1, + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="Nest", + number=1, + ) + assert result == mock_milestone + + @patch("apps.api.rest.v0.milestone.MilestoneModel.objects") + def test_get_milestone_not_found(self, mock_objects): + """Test getting a milestone that does not exist returns 404.""" + mock_request = MagicMock() + + mock_objects.get.side_effect = MilestoneModel.DoesNotExist + + result = get_milestone( + mock_request, + organization_id="OWASP", + repository_id="NonExistent", + milestone_id=999, + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="NonExistent", + number=999, + ) + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Milestone not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/organization_test.py b/backend/tests/apps/api/rest/v0/organization_test.py index 73ae839e66..88b61c9e9b 100644 --- a/backend/tests/apps/api/rest/v0/organization_test.py +++ b/backend/tests/apps/api/rest/v0/organization_test.py @@ -1,8 +1,14 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.organization import OrganizationDetail +from apps.api.rest.v0.organization import ( + OrganizationDetail, + get_organization, + list_organization, +) class TestOrganizationSchema: @@ -36,3 +42,89 @@ def test_organization_schema(self, organization_data): assert organization.login == organization_data["login"] assert organization.name == organization_data["name"] assert organization.updated_at == datetime.fromisoformat(organization_data["updated_at"]) + + +class TestListOrganization: + """Test cases for list_organization endpoint.""" + + @patch("apps.api.rest.v0.organization.OrganizationModel.objects") + def test_list_organization_with_custom_ordering(self, mock_objects): + """Test listing organizations with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_base_filter = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_objects.filter.return_value = mock_base_filter + mock_base_filter.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_organization(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.filter.assert_called_once_with(is_owasp_related_organization=True) + mock_base_filter.order_by.assert_called_once_with("created_at") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + @patch("apps.api.rest.v0.organization.OrganizationModel.objects") + def test_list_organization_with_default_ordering(self, mock_objects): + """Test listing organizations with default ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_base_filter = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_objects.filter.return_value = mock_base_filter + mock_base_filter.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_organization(mock_request, filters=mock_filters, ordering=None) + + mock_objects.filter.assert_called_once_with(is_owasp_related_organization=True) + mock_base_filter.order_by.assert_called_once_with("-created_at") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + +class TestGetOrganization: + """Test cases for get_organization endpoint.""" + + @patch("apps.api.rest.v0.organization.OrganizationModel.objects") + def test_get_organization_found(self, mock_objects): + """Test getting an organization that exists.""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_org = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_org + + result = get_organization(mock_request, organization_id="OWASP") + + mock_objects.filter.assert_called_once_with( + is_owasp_related_organization=True, + login__iexact="OWASP", + ) + mock_filter.first.assert_called_once() + assert result == mock_org + + @patch("apps.api.rest.v0.organization.OrganizationModel.objects") + def test_get_organization_not_found(self, mock_objects): + """Test getting an organization that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_organization(mock_request, organization_id="NonExistent") + + mock_objects.filter.assert_called_once_with( + is_owasp_related_organization=True, + login__iexact="NonExistent", + ) + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Organization not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/pagination_test.py b/backend/tests/apps/api/rest/v0/pagination_test.py new file mode 100644 index 0000000000..da5addff8f --- /dev/null +++ b/backend/tests/apps/api/rest/v0/pagination_test.py @@ -0,0 +1,293 @@ +from unittest.mock import MagicMock + +import pytest +from django.http import Http404 + +from apps.api.rest.v0.pagination import CustomPagination + + +class TestCustomPagination: + """Test cases for CustomPagination class.""" + + def test_paginate_first_page(self): + """Test pagination on the first page.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 25 + mock_queryset.__getitem__ = MagicMock(return_value=["item1", "item2", "item3"]) + + mock_pagination = MagicMock() + mock_pagination.page = 1 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 1 + assert result["total_count"] == 25 + assert result["total_pages"] == 3 + assert result["has_next"] is True + assert result["has_previous"] is False + assert result["items"] == ["item1", "item2", "item3"] + mock_queryset.__getitem__.assert_called_once_with(slice(0, 10)) + + def test_paginate_middle_page(self): + """Test pagination on a middle page.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 35 + mock_queryset.__getitem__ = MagicMock(return_value=["item11", "item12"]) + + mock_pagination = MagicMock() + mock_pagination.page = 2 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 2 + assert result["total_count"] == 35 + assert result["total_pages"] == 4 + assert result["has_next"] is True + assert result["has_previous"] is True + assert result["items"] == ["item11", "item12"] + mock_queryset.__getitem__.assert_called_once_with(slice(10, 20)) + + def test_paginate_last_page(self): + """Test pagination on the last page.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 25 + mock_queryset.__getitem__ = MagicMock( + return_value=["item21", "item22", "item23", "item24", "item25"] + ) + + mock_pagination = MagicMock() + mock_pagination.page = 3 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 3 + assert result["total_count"] == 25 + assert result["total_pages"] == 3 + assert result["has_next"] is False + assert result["has_previous"] is True + assert result["items"] == ["item21", "item22", "item23", "item24", "item25"] + mock_queryset.__getitem__.assert_called_once_with(slice(20, 30)) + + def test_paginate_single_page(self): + """Test pagination when all items fit on a single page.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 5 + mock_queryset.__getitem__ = MagicMock( + return_value=["item1", "item2", "item3", "item4", "item5"] + ) + + mock_pagination = MagicMock() + mock_pagination.page = 1 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 1 + assert result["total_count"] == 5 + assert result["total_pages"] == 1 + assert result["has_next"] is False + assert result["has_previous"] is False + assert result["items"] == ["item1", "item2", "item3", "item4", "item5"] + mock_queryset.__getitem__.assert_called_once_with(slice(0, 10)) + + def test_paginate_empty_queryset_page_one(self): + """Test pagination with empty queryset on page 1 (should succeed).""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 0 + mock_queryset.__getitem__ = MagicMock(return_value=[]) + + mock_pagination = MagicMock() + mock_pagination.page = 1 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 1 + assert result["total_count"] == 0 + assert result["total_pages"] == 1 # max(1, 0) = 1 + assert result["has_next"] is False + assert result["has_previous"] is False + assert result["items"] == [] + mock_queryset.__getitem__.assert_called_once_with(slice(0, 10)) + + def test_paginate_empty_queryset_page_two(self): + """Test pagination with empty queryset on page 2 (should raise Http404).""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 0 + + mock_pagination = MagicMock() + mock_pagination.page = 2 + mock_pagination.page_size = 10 + + with pytest.raises(Http404) as exc_info: + paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert "Page 2 not found. Valid pages are 1 to 1." in str(exc_info.value) + + def test_paginate_page_out_of_range(self): + """Test pagination when requested page exceeds total pages.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 25 + + mock_pagination = MagicMock() + mock_pagination.page = 5 + mock_pagination.page_size = 10 + + with pytest.raises(Http404) as exc_info: + paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert "Page 5 not found. Valid pages are 1 to 3." in str(exc_info.value) + + def test_paginate_partial_last_page(self): + """Test pagination when last page has fewer items than page_size.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 23 + mock_queryset.__getitem__ = MagicMock(return_value=["item21", "item22", "item23"]) + + mock_pagination = MagicMock() + mock_pagination.page = 3 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 3 + assert result["total_count"] == 23 + assert result["total_pages"] == 3 + assert result["has_next"] is False + assert result["has_previous"] is True + assert result["items"] == ["item21", "item22", "item23"] + mock_queryset.__getitem__.assert_called_once_with(slice(20, 30)) + + def test_paginate_exact_multiple_pages(self): + """Test pagination when total items is exact multiple of page_size.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 30 + mock_queryset.__getitem__ = MagicMock( + return_value=[ + "item21", + "item22", + "item23", + "item24", + "item25", + "item26", + "item27", + "item28", + "item29", + "item30", + ] + ) + + mock_pagination = MagicMock() + mock_pagination.page = 3 + mock_pagination.page_size = 10 + + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + + assert result["current_page"] == 3 + assert result["total_count"] == 30 + assert result["total_pages"] == 3 + assert result["has_next"] is False + assert result["has_previous"] is True + assert result["items"] == [ + "item21", + "item22", + "item23", + "item24", + "item25", + "item26", + "item27", + "item28", + "item29", + "item30", + ] + mock_queryset.__getitem__.assert_called_once_with(slice(20, 30)) + + def test_paginate_offset_calculation(self): + """Test that offset calculation is correct for various pages.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.count.return_value = 100 + mock_queryset.__getitem__ = MagicMock(return_value=[]) + + mock_pagination = MagicMock() + mock_pagination.page_size = 20 + + # Page 1: offset = (1-1) * 20 = 0 + mock_pagination.page = 1 + paginator.paginate_queryset(mock_queryset, mock_pagination) + mock_queryset.__getitem__.assert_called_with(slice(0, 20)) + mock_queryset.__getitem__.reset_mock() + + # Page 3: offset = (3-1) * 20 = 40 + mock_pagination.page = 3 + paginator.paginate_queryset(mock_queryset, mock_pagination) + mock_queryset.__getitem__.assert_called_with(slice(40, 60)) + mock_queryset.__getitem__.reset_mock() + + # Page 5: offset = (5-1) * 20 = 80 + mock_pagination.page = 5 + paginator.paginate_queryset(mock_queryset, mock_pagination) + mock_queryset.__getitem__.assert_called_with(slice(80, 100)) + + def test_paginate_total_pages_calculation(self): + """Test total_pages calculation for various edge cases.""" + paginator = CustomPagination() + + mock_queryset = MagicMock() + mock_queryset.__getitem__ = MagicMock(return_value=[]) + + mock_pagination = MagicMock() + mock_pagination.page = 1 + mock_pagination.page_size = 10 + + # 0 items: total_pages = max(1, (0 + 10 - 1) // 10) = max(1, 0) = 1 + mock_queryset.count.return_value = 0 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 1 + + # 1 item: total_pages = max(1, (1 + 10 - 1) // 10) = max(1, 1) = 1 + mock_queryset.count.return_value = 1 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 1 + + # 10 items: total_pages = max(1, (10 + 10 - 1) // 10) = max(1, 1) = 1 + mock_queryset.count.return_value = 10 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 1 + + # 11 items: total_pages = max(1, (11 + 10 - 1) // 10) = max(1, 2) = 2 + mock_queryset.count.return_value = 11 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 2 + + # 99 items: total_pages = max(1, (99 + 10 - 1) // 10) = max(1, 10) = 10 + mock_queryset.count.return_value = 99 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 10 + + # 100 items: total_pages = max(1, (100 + 10 - 1) // 10) = max(1, 10) = 10 + mock_queryset.count.return_value = 100 + result = paginator.paginate_queryset(mock_queryset, mock_pagination) + assert result["total_pages"] == 10 diff --git a/backend/tests/apps/api/rest/v0/project_test.py b/backend/tests/apps/api/rest/v0/project_test.py index 112b41fb5c..4aed791978 100644 --- a/backend/tests/apps/api/rest/v0/project_test.py +++ b/backend/tests/apps/api/rest/v0/project_test.py @@ -1,8 +1,10 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.project import ProjectDetail +from apps.api.rest.v0.project import ProjectDetail, get_project, list_projects @pytest.mark.parametrize( @@ -41,3 +43,113 @@ def __init__(self, data): assert project.level == project_data["level"] assert project.name == project_data["name"] assert project.updated_at == datetime.fromisoformat(project_data["updated_at"]) + + +class TestListProjects: + """Test cases for list_projects endpoint.""" + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_list_projects_with_custom_ordering(self, mock_active_projects): + """Test listing projects with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_active_projects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_projects(mock_request, filters=mock_filters, ordering="created_at") + + mock_active_projects.order_by.assert_called_once_with( + "created_at", "-stars_count", "-forks_count" + ) + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_list_projects_with_default_ordering(self, mock_active_projects): + """Test listing projects with default ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_active_projects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_projects(mock_request, filters=mock_filters, ordering=None) + + mock_active_projects.order_by.assert_called_once_with( + "-level_raw", "-stars_count", "-forks_count" + ) + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + +class TestGetProject: + """Test cases for get_project endpoint.""" + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_get_project_without_prefix(self, mock_active_projects): + """Test getting a project without 'www-project-' prefix (should add it).""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_project = MagicMock() + + mock_active_projects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_project + + result = get_project(mock_request, project_id="Nest") + + mock_active_projects.filter.assert_called_once_with(key__iexact="www-project-Nest") + mock_filter.first.assert_called_once() + assert result == mock_project + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_get_project_with_prefix(self, mock_active_projects): + """Test getting a project that already has 'www-project-' prefix.""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_project = MagicMock() + + mock_active_projects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_project + + result = get_project(mock_request, project_id="www-project-Nest") + + mock_active_projects.filter.assert_called_once_with(key__iexact="www-project-Nest") + mock_filter.first.assert_called_once() + assert result == mock_project + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_get_project_uppercase_prefix(self, mock_active_projects): + """Test getting a project with uppercase prefix (case-insensitive detection).""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_project = MagicMock() + + mock_active_projects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_project + + result = get_project(mock_request, project_id="WWW-PROJECT-Nest") + + mock_active_projects.filter.assert_called_once_with(key__iexact="WWW-PROJECT-Nest") + mock_filter.first.assert_called_once() + assert result == mock_project + + @patch("apps.api.rest.v0.project.ProjectModel.active_projects") + def test_get_project_not_found(self, mock_active_projects): + """Test getting a project that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_active_projects.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_project(mock_request, project_id="NonExistent") + + mock_active_projects.filter.assert_called_once_with(key__iexact="www-project-NonExistent") + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Project not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/release_test.py b/backend/tests/apps/api/rest/v0/release_test.py index 8aa5d649a5..715c2fb162 100644 --- a/backend/tests/apps/api/rest/v0/release_test.py +++ b/backend/tests/apps/api/rest/v0/release_test.py @@ -1,8 +1,11 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.release import ReleaseDetail +from apps.api.rest.v0.release import ReleaseDetail, get_release, list_release +from apps.github.models.release import Release as ReleaseModel class TestReleaseSchema: @@ -33,3 +36,194 @@ def test_release_schema(self, release_data): assert release.name == release_data["name"] assert release.published_at == datetime.fromisoformat(release_data["published_at"]) assert release.tag_name == release_data["tag_name"] + + +class TestListRelease: + """Test cases for list_release endpoint.""" + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_list_release_with_custom_ordering(self, mock_objects): + """Test listing releases with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.tag_name = None + + mock_exclude = MagicMock() + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.exclude.return_value = mock_exclude + mock_exclude.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_release(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.exclude.assert_called_once_with(published_at__isnull=True) + mock_exclude.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select.order_by.assert_called_once_with("created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_list_release_with_default_ordering(self, mock_objects): + """Test listing releases with default ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.tag_name = None + + mock_exclude = MagicMock() + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.exclude.return_value = mock_exclude + mock_exclude.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_release(mock_request, filters=mock_filters, ordering=None) + + mock_objects.exclude.assert_called_once_with(published_at__isnull=True) + mock_exclude.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select.order_by.assert_called_once_with("-published_at", "-created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_list_release_with_organization_filter(self, mock_objects): + """Test listing releases with organization filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = "OWASP" + mock_filters.repository = None + mock_filters.tag_name = None + + mock_exclude = MagicMock() + mock_select = MagicMock() + mock_filter_org = MagicMock() + mock_ordered = MagicMock() + + mock_objects.exclude.return_value = mock_exclude + mock_exclude.select_related.return_value = mock_select + mock_select.filter.return_value = mock_filter_org + mock_filter_org.order_by.return_value = mock_ordered + + result = list_release(mock_request, filters=mock_filters, ordering=None) + + mock_objects.exclude.assert_called_once_with(published_at__isnull=True) + mock_exclude.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select.filter.assert_called_once_with(repository__organization__login__iexact="OWASP") + mock_filter_org.order_by.assert_called_once_with("-published_at", "-created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_list_release_with_repository_filter(self, mock_objects): + """Test listing releases with repository filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = "Nest" + mock_filters.tag_name = None + + mock_exclude = MagicMock() + mock_select = MagicMock() + mock_filter_repo = MagicMock() + mock_ordered = MagicMock() + + mock_objects.exclude.return_value = mock_exclude + mock_exclude.select_related.return_value = mock_select + mock_select.filter.return_value = mock_filter_repo + mock_filter_repo.order_by.return_value = mock_ordered + + result = list_release(mock_request, filters=mock_filters, ordering=None) + + mock_objects.exclude.assert_called_once_with(published_at__isnull=True) + mock_exclude.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select.filter.assert_called_once_with(repository__name__iexact="Nest") + mock_filter_repo.order_by.assert_called_once_with("-published_at", "-created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_list_release_with_tag_name_filter(self, mock_objects): + """Test listing releases with tag_name filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization = None + mock_filters.repository = None + mock_filters.tag_name = "v1.0.0" + + mock_exclude = MagicMock() + mock_select = MagicMock() + mock_filter_tag = MagicMock() + mock_ordered = MagicMock() + + mock_objects.exclude.return_value = mock_exclude + mock_exclude.select_related.return_value = mock_select + mock_select.filter.return_value = mock_filter_tag + mock_filter_tag.order_by.return_value = mock_ordered + + result = list_release(mock_request, filters=mock_filters, ordering=None) + + mock_objects.exclude.assert_called_once_with(published_at__isnull=True) + mock_exclude.select_related.assert_called_once_with( + "repository", "repository__organization" + ) + mock_select.filter.assert_called_once_with(tag_name="v1.0.0") + mock_filter_tag.order_by.assert_called_once_with("-published_at", "-created_at", "id") + assert result == mock_ordered + + +class TestGetRelease: + """Test cases for get_release endpoint.""" + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_get_release_found(self, mock_objects): + """Test getting a release that exists.""" + mock_request = MagicMock() + mock_release = MagicMock() + + mock_objects.get.return_value = mock_release + + result = get_release( + mock_request, + organization_id="OWASP", + repository_id="Nest", + release_id="v1.0.0", + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="Nest", + tag_name="v1.0.0", + ) + assert result == mock_release + + @patch("apps.api.rest.v0.release.ReleaseModel.objects") + def test_get_release_not_found(self, mock_objects): + """Test getting a release that does not exist returns 404.""" + mock_request = MagicMock() + + mock_objects.get.side_effect = ReleaseModel.DoesNotExist + + result = get_release( + mock_request, + organization_id="OWASP", + repository_id="NonExistent", + release_id="v99.99.99", + ) + + mock_objects.get.assert_called_once_with( + repository__organization__login__iexact="OWASP", + repository__name__iexact="NonExistent", + tag_name="v99.99.99", + ) + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Release not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/repository_test.py b/backend/tests/apps/api/rest/v0/repository_test.py index 35c896f9b6..cc18499550 100644 --- a/backend/tests/apps/api/rest/v0/repository_test.py +++ b/backend/tests/apps/api/rest/v0/repository_test.py @@ -1,8 +1,11 @@ from datetime import datetime +from http import HTTPStatus +from unittest.mock import MagicMock, patch import pytest -from apps.api.rest.v0.repository import RepositoryDetail +from apps.api.rest.v0.repository import RepositoryDetail, get_repository, list_repository +from apps.github.models.repository import Repository as RepositoryModel class TestRepositorySchema: @@ -30,3 +33,149 @@ def test_repository_schema(self, repository_data): assert repository.description == repository_data["description"] assert repository.name == repository_data["name"] assert repository.updated_at == datetime.fromisoformat(repository_data["updated_at"]) + + +class TestListRepository: + """Test cases for list_repository endpoint.""" + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_list_repository_with_custom_ordering(self, mock_objects): + """Test listing repositories with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization_id = None + + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_repository(mock_request, filters=mock_filters, ordering="created_at") + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.order_by.assert_called_once_with("created_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_list_repository_ordering_by_updated_at(self, mock_objects): + """Test listing repositories with ordering by updated_at avoids double ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization_id = None + + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_repository(mock_request, filters=mock_filters, ordering="updated_at") + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.order_by.assert_called_once_with("updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_list_repository_with_default_ordering(self, mock_objects): + """Test listing repositories with default ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization_id = None + + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_repository(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_list_repository_with_organization_filter(self, mock_objects): + """Test listing repositories with organization filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization_id = "OWASP" + + mock_select = MagicMock() + mock_filter = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.filter.return_value = mock_filter + mock_filter.order_by.return_value = mock_ordered + + result = list_repository(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.filter.assert_called_once_with(organization__login__iexact="OWASP") + mock_filter.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_list_repository_without_organization_filter(self, mock_objects): + """Test listing repositories without organization filter.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_filters.organization_id = None + + mock_select = MagicMock() + mock_ordered = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.order_by.return_value = mock_ordered + + result = list_repository(mock_request, filters=mock_filters, ordering=None) + + mock_objects.select_related.assert_called_once_with("organization") + # Filter should not be called when organization_id is None + mock_select.filter.assert_not_called() + mock_select.order_by.assert_called_once_with("-created_at", "-updated_at", "id") + assert result == mock_ordered + + +class TestGetRepository: + """Test cases for get_repository endpoint.""" + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_get_repository_found(self, mock_objects): + """Test getting a repository that exists.""" + mock_request = MagicMock() + mock_select = MagicMock() + mock_repo = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.get.return_value = mock_repo + + result = get_repository(mock_request, organization_id="OWASP", repository_id="Nest") + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.get.assert_called_once_with( + organization__login__iexact="OWASP", + name__iexact="Nest", + ) + assert result == mock_repo + + @patch("apps.api.rest.v0.repository.RepositoryModel.objects") + def test_get_repository_not_found(self, mock_objects): + """Test getting a repository that does not exist returns 404.""" + mock_request = MagicMock() + mock_select = MagicMock() + + mock_objects.select_related.return_value = mock_select + mock_select.get.side_effect = RepositoryModel.DoesNotExist + + result = get_repository(mock_request, organization_id="OWASP", repository_id="NonExistent") + + mock_objects.select_related.assert_called_once_with("organization") + mock_select.get.assert_called_once_with( + organization__login__iexact="OWASP", + name__iexact="NonExistent", + ) + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Repository not found" in result.content diff --git a/backend/tests/apps/api/rest/v0/sponsor_test.py b/backend/tests/apps/api/rest/v0/sponsor_test.py index 24a7ebadf4..a90573ab23 100644 --- a/backend/tests/apps/api/rest/v0/sponsor_test.py +++ b/backend/tests/apps/api/rest/v0/sponsor_test.py @@ -1,6 +1,9 @@ +from http import HTTPStatus +from unittest.mock import MagicMock, patch + import pytest -from apps.api.rest.v0.sponsor import SponsorDetail +from apps.api.rest.v0.sponsor import SponsorDetail, get_sponsor, list_sponsors class TestSponsorSchema: @@ -63,3 +66,94 @@ def test_sponsor_schema_with_minimal_data(self): assert sponsor.job_url == "" assert sponsor.key == "test-sponsor" assert sponsor.name == "Test Sponsor" + + +class TestListSponsors: + """Test cases for list_sponsors endpoint.""" + + @patch("apps.api.rest.v0.sponsor.SponsorModel.objects") + def test_list_sponsors_with_custom_ordering(self, mock_objects): + """Test listing sponsors with custom ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_objects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_sponsors(mock_request, filters=mock_filters, ordering="name") + + mock_objects.order_by.assert_called_once_with("name") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + @patch("apps.api.rest.v0.sponsor.SponsorModel.objects") + def test_list_sponsors_with_default_ordering(self, mock_objects): + """Test listing sponsors with default ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_objects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_sponsors(mock_request, filters=mock_filters, ordering=None) + + mock_objects.order_by.assert_called_once_with("name") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + @patch("apps.api.rest.v0.sponsor.SponsorModel.objects") + def test_list_sponsors_with_reverse_ordering(self, mock_objects): + """Test listing sponsors with reverse ordering.""" + mock_request = MagicMock() + mock_filters = MagicMock() + mock_ordered = MagicMock() + mock_final = MagicMock() + + mock_objects.order_by.return_value = mock_ordered + mock_filters.filter.return_value = mock_final + + result = list_sponsors(mock_request, filters=mock_filters, ordering="-name") + + mock_objects.order_by.assert_called_once_with("-name") + mock_filters.filter.assert_called_once_with(mock_ordered) + assert result == mock_final + + +class TestGetSponsor: + """Test cases for get_sponsor endpoint.""" + + @patch("apps.api.rest.v0.sponsor.SponsorModel.objects") + def test_get_sponsor_found(self, mock_objects): + """Test getting a sponsor that exists.""" + mock_request = MagicMock() + mock_filter = MagicMock() + mock_sponsor = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = mock_sponsor + + result = get_sponsor(mock_request, sponsor_id="adobe") + + mock_objects.filter.assert_called_once_with(key__iexact="adobe") + mock_filter.first.assert_called_once() + assert result == mock_sponsor + + @patch("apps.api.rest.v0.sponsor.SponsorModel.objects") + def test_get_sponsor_not_found(self, mock_objects): + """Test getting a sponsor that does not exist returns 404.""" + mock_request = MagicMock() + mock_filter = MagicMock() + + mock_objects.filter.return_value = mock_filter + mock_filter.first.return_value = None + + result = get_sponsor(mock_request, sponsor_id="nonexistent") + + mock_objects.filter.assert_called_once_with(key__iexact="nonexistent") + mock_filter.first.assert_called_once() + assert result.status_code == HTTPStatus.NOT_FOUND + assert b"Sponsor not found" in result.content