Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
10adfe2
test: improve cache decorator coverage to 100%
EclipseAditya Oct 31, 2025
87f450a
test: improve api_key mutation coverage to 100%
EclipseAditya Oct 31, 2025
e6ac712
test: improve chapter endpoint coverage to 100%
EclipseAditya Oct 31, 2025
31c4af8
test: improve committee endpoint coverage to 100%
EclipseAditya Oct 31, 2025
d2e2ae1
test: improve event endpoint coverage to 100%
EclipseAditya Oct 31, 2025
4aa9cb4
test: improve issue endpoint coverage to 100%
EclipseAditya Oct 31, 2025
394e4e9
test: improve label endpoint coverage to 100%
EclipseAditya Oct 31, 2025
81459c6
test: improve member endpoint coverage to 100%
EclipseAditya Oct 31, 2025
32d310d
test: improve milestone endpoint coverage to 100%
EclipseAditya Nov 1, 2025
4d91bab
test: improve organization endpoint coverage to 100%
EclipseAditya Nov 1, 2025
6de85f2
test: add comprehensive coverage for CustomPagination utility
EclipseAditya Nov 1, 2025
1689530
test: improve project endpoint coverage to 100%
EclipseAditya Nov 1, 2025
9c0c191
test: add sponsor endpoint tests and fix CodeRabbit issues
EclipseAditya Nov 1, 2025
593395f
test: improve repository endpoint coverage to 100%
EclipseAditya Nov 1, 2025
c3ced64
test: improve release endpoint coverage to 100%
EclipseAditya Nov 1, 2025
951e409
fix check
EclipseAditya Nov 1, 2025
8c10abe
Merge branch 'main' into feature/improve-api-test-coverage
EclipseAditya Nov 2, 2025
2322d40
addressed bugs found during test coverage improvements
EclipseAditya Nov 2, 2025
f8912b3
address bugs and inconsistencies in REST API v0 endpoints
EclipseAditya Nov 2, 2025
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
84 changes: 83 additions & 1 deletion backend/tests/apps/api/decorators/cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
64 changes: 63 additions & 1 deletion backend/tests/apps/api/internal/mutations/api_key_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down
111 changes: 110 additions & 1 deletion backend/tests/apps/api/rest/v0/chapter_test.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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 not detected and gets added again."""
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="WWW-CHAPTER-London")

mock_active_chapters.filter.assert_called_once_with(
key__iexact="www-chapter-WWW-CHAPTER-London"
)
mock_filter.first.assert_called_once()
assert result.status_code == HTTPStatus.NOT_FOUND
Loading