diff --git a/backend/apps/github/api/internal/nodes/repository.py b/backend/apps/github/api/internal/nodes/repository.py index 6a8c687d01..0f2cc5cbeb 100644 --- a/backend/apps/github/api/internal/nodes/repository.py +++ b/backend/apps/github/api/internal/nodes/repository.py @@ -17,6 +17,7 @@ RECENT_ISSUES_LIMIT = 5 RECENT_RELEASES_LIMIT = 5 +MAX_RECENT_MILESTONES_LIMIT = 100 @strawberry_django.type( @@ -69,6 +70,9 @@ def project( @strawberry_django.field(prefetch_related=["milestones"]) def recent_milestones(self, root: Repository, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" + if limit < 0: + return [] + limit = min(limit, MAX_RECENT_MILESTONES_LIMIT) return root.recent_milestones.order_by("-created_at")[:limit] @strawberry_django.field(prefetch_related=["releases"]) diff --git a/backend/apps/owasp/api/internal/nodes/project.py b/backend/apps/owasp/api/internal/nodes/project.py index f5da84653f..922c577f19 100644 --- a/backend/apps/owasp/api/internal/nodes/project.py +++ b/backend/apps/owasp/api/internal/nodes/project.py @@ -20,7 +20,7 @@ RECENT_RELEASES_LIMIT = 5 RECENT_PULL_REQUESTS_LIMIT = 5 -MAX_LIMIT = 1000 +MAX_LIMIT = 100 @strawberry_django.type( @@ -54,7 +54,7 @@ def health_metrics_list( ) -> list[ProjectHealthMetricsNode]: """Resolve project health metrics.""" return ( - root.health_metrics.order_by("nest_created_at")[:limit] + root.health_metrics.order_by("-nest_created_at")[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] ) diff --git a/backend/apps/owasp/api/internal/nodes/snapshot.py b/backend/apps/owasp/api/internal/nodes/snapshot.py index 4e45ab89eb..8be560bbe6 100644 --- a/backend/apps/owasp/api/internal/nodes/snapshot.py +++ b/backend/apps/owasp/api/internal/nodes/snapshot.py @@ -11,6 +11,10 @@ from apps.owasp.models.snapshot import Snapshot RECENT_ISSUES_LIMIT = 100 +RECENT_RELEASES_LIMIT = 50 +RECENT_USERS_LIMIT = 50 +RECENT_PROJECTS_LIMIT = 50 +RECENT_CHAPTERS_LIMIT = 50 @strawberry_django.type( @@ -25,8 +29,6 @@ class SnapshotNode(strawberry.relay.Node): """Snapshot node.""" - new_chapters: list[ChapterNode] = strawberry_django.field() - @strawberry_django.field def key(self, root: Snapshot) -> str: """Resolve key.""" @@ -40,14 +42,19 @@ def new_issues(self, root: Snapshot) -> list[IssueNode]: @strawberry_django.field(prefetch_related=["new_projects"]) def new_projects(self, root: Snapshot) -> list[ProjectNode]: """Resolve new projects.""" - return root.new_projects.order_by("-created_at") + return root.new_projects.order_by("-created_at")[:RECENT_PROJECTS_LIMIT] @strawberry_django.field(prefetch_related=["new_releases"]) def new_releases(self, root: Snapshot) -> list[ReleaseNode]: """Resolve new releases.""" - return root.new_releases.order_by("-published_at") + return root.new_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] @strawberry_django.field(prefetch_related=["new_users"]) def new_users(self, root: Snapshot) -> list[UserNode]: """Resolve new users.""" - return root.new_users.order_by("-created_at") + return root.new_users.order_by("-created_at")[:RECENT_USERS_LIMIT] + + @strawberry_django.field(prefetch_related=["new_chapters"]) + def new_chapters(self, root: Snapshot) -> list[ChapterNode]: + """Resolve new chapters.""" + return root.new_chapters.order_by("-created_at")[:RECENT_CHAPTERS_LIMIT] diff --git a/backend/apps/owasp/api/internal/queries/project.py b/backend/apps/owasp/api/internal/queries/project.py index af4db3c009..667124cf5b 100644 --- a/backend/apps/owasp/api/internal/queries/project.py +++ b/backend/apps/owasp/api/internal/queries/project.py @@ -8,10 +8,11 @@ from apps.owasp.api.internal.nodes.project import ProjectNode from apps.owasp.models.project import Project -MAX_RECENT_PROJECTS_LIMIT = 1000 +MAX_RECENT_PROJECTS_LIMIT = 100 MAX_SEARCH_QUERY_LENGTH = 100 MIN_SEARCH_QUERY_LENGTH = 3 -SEARCH_PROJECTS_LIMIT = 3 +SEARCH_PROJECTS_LIMIT = 100 +MAX_PROJECT_KEY_LENGTH = 50 @strawberry.type @@ -29,8 +30,15 @@ def project(self, key: str) -> ProjectNode | None: ProjectNode | None: The project node if found, otherwise None. """ + normalized_key = key.strip() + + if not normalized_key or len(normalized_key) > MAX_PROJECT_KEY_LENGTH: + return None + try: - return Project.objects.get(key=f"www-project-{key}") + return Project.objects.only("id", "key", "name", "is_active", "created_at").get( + key=f"www-project-{normalized_key}" + ) except Project.DoesNotExist: return None @@ -45,10 +53,15 @@ def recent_projects(self, limit: int = 8) -> list[ProjectNode]: list[ProjectNode]: A list of recent active projects. """ - return ( - Project.objects.filter(is_active=True).order_by("-created_at")[:limit] - if (limit := min(limit, MAX_RECENT_PROJECTS_LIMIT)) > 0 - else [] + if limit <= 0: + return [] + + limit = min(max(limit, 1), MAX_RECENT_PROJECTS_LIMIT) + + return list( + Project.objects.filter(is_active=True) + .only("id", "key", "name", "created_at", "is_active") + .order_by("-created_at")[:limit] ) @strawberry_django.field @@ -61,14 +74,21 @@ def search_projects(self, query: str) -> list[ProjectNode]: ): return [] - return Project.objects.filter( - is_active=True, - name__icontains=cleaned_query, - ).order_by("name")[:SEARCH_PROJECTS_LIMIT] + return list( + Project.objects.filter( + is_active=True, + name__icontains=cleaned_query, + ) + .only("id", "key", "name", "is_active") + .order_by("name")[:SEARCH_PROJECTS_LIMIT] + ) @strawberry_django.field def is_project_leader(self, info: strawberry.Info, login: str) -> bool: """Check if a GitHub login or name is listed as a project leader.""" + if not login or not login.strip(): + return False + try: github_user = GithubUser.objects.get(login=login) except GithubUser.DoesNotExist: diff --git a/backend/apps/owasp/api/internal/queries/snapshot.py b/backend/apps/owasp/api/internal/queries/snapshot.py index d649acad34..5bc8fd0943 100644 --- a/backend/apps/owasp/api/internal/queries/snapshot.py +++ b/backend/apps/owasp/api/internal/queries/snapshot.py @@ -6,7 +6,7 @@ from apps.owasp.api.internal.nodes.snapshot import SnapshotNode from apps.owasp.models.snapshot import Snapshot -MAX_LIMIT = 100 +MAX_LIMIT = 10 @strawberry.type @@ -27,12 +27,15 @@ def snapshot(self, key: str) -> SnapshotNode | None: @strawberry_django.field def snapshots(self, limit: int = 12) -> list[SnapshotNode]: """Resolve snapshots.""" - return ( + if limit <= 0: + return [] + + limit = min(max(limit, 1), MAX_LIMIT) + + return list( Snapshot.objects.filter( status=Snapshot.Status.COMPLETED, - ).order_by( - "-created_at", - )[:limit] - if (limit := min(limit, MAX_LIMIT)) > 0 - else [] + ) + .only("id", "key", "title", "created_at") + .order_by("-created_at")[:limit] ) diff --git a/backend/tests/apps/owasp/api/internal/queries/project_test.py b/backend/tests/apps/owasp/api/internal/queries/project_test.py index a5b05c6eb1..460e6a7350 100644 --- a/backend/tests/apps/owasp/api/internal/queries/project_test.py +++ b/backend/tests/apps/owasp/api/internal/queries/project_test.py @@ -47,22 +47,26 @@ def mock_project(self): def test_resolve_project_existing(self, mock_project, mock_info): """Test resolving an existing project.""" - with patch("apps.owasp.models.project.Project.objects.get") as mock_get: - mock_get.return_value = mock_project + with patch("apps.owasp.models.project.Project.objects.only") as mock_only: + mock_qs = Mock() + mock_qs.get.return_value = mock_project + mock_only.return_value = mock_qs query = ProjectQuery() result = query.__class__.__dict__["project"](query, key="test-project") assert result == mock_project - mock_get.assert_called_once_with(key="www-project-test-project") + mock_qs.get.assert_called_once_with(key="www-project-test-project") def test_resolve_project_not_found(self, mock_info): """Test resolving a non-existent project.""" - with patch("apps.owasp.models.project.Project.objects.get") as mock_get: - mock_get.side_effect = Project.DoesNotExist + with patch("apps.owasp.models.project.Project.objects.only") as mock_only: + mock_qs = Mock() + mock_qs.get.side_effect = Project.DoesNotExist + mock_only.return_value = mock_qs query = ProjectQuery() result = query.__class__.__dict__["project"](query, key="non-existent") assert result is None - mock_get.assert_called_once_with(key="www-project-non-existent") + mock_qs.get.assert_called_once_with(key="www-project-non-existent")