From d5e8676c5717fcf26498f7c4c77b2226beef31df Mon Sep 17 00:00:00 2001 From: TejasLamba2006 Date: Mon, 9 Nov 2026 22:00:22 +0530 Subject: [PATCH 1/4] fix(environments): add CustomPagination to EnvironmentViewSet EnvironmentViewSet had no pagination_class, so it fell back to DRF's default of 10 per page and ignored the page_size query parameter. - Set pagination_class = CustomPagination on EnvironmentViewSet so callers can pass page_size and receive a standard paginated response - Drop the getAllEnvironments RTK endpoint (which used fetchAllPages to work around the missing pagination) in favour of getEnvironments - Update useGroupedBreakdown to use useGetEnvironmentsQuery and read .results from the paged response - Remove the now-unused getAllEnvironments request/response types - Add backend tests covering the paginated response shape and that page_size is respected --- api/environments/views.py | 2 + .../test_unit_environments_views.py | 43 +++++++++++++++++++ frontend/common/services/useEnvironment.ts | 18 +------- frontend/common/types/requests.ts | 3 -- frontend/common/types/responses.ts | 1 - .../UsageBreakdown/useGroupedBreakdown.ts | 6 +-- 6 files changed, 49 insertions(+), 24 deletions(-) diff --git a/api/environments/views.py b/api/environments/views.py index 8fda3026fe6d..5533bc2d6bb2 100644 --- a/api/environments/views.py +++ b/api/environments/views.py @@ -19,6 +19,7 @@ from rest_framework.serializers import BaseSerializer from rest_framework.viewsets import GenericViewSet +from app.pagination import CustomPagination from core.models import AbstractBaseExportableModel from environments.permissions.permissions import ( EnvironmentAdminPermission, @@ -86,6 +87,7 @@ ) class EnvironmentViewSet(viewsets.ModelViewSet): # type: ignore[type-arg] lookup_field = "api_key" + pagination_class = CustomPagination permission_classes = [EnvironmentPermissions] def get_serializer_class(self): # type: ignore[no-untyped-def] diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index 30b0a9a71cd4..c7312a19469e 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -1395,3 +1395,46 @@ def test_create_environment__required_metadata_on_other_project__returns_201( # Then assert response.status_code == status.HTTP_201_CREATED + + +def test_list_environments__returns_paginated_response( + admin_client_new: APIClient, + project: Project, +) -> None: + # Given + Environment.objects.create(name="Env One", project=project) + Environment.objects.create(name="Env Two", project=project) + url = reverse("api-v1:environments:environment-list") + + # When + response = admin_client_new.get( + url, data={"project": project.id}, content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_200_OK + assert "results" in response.json() + assert "count" in response.json() + + +def test_list_environments__page_size_param_is_respected( + admin_client_new: APIClient, + project: Project, +) -> None: + # Given + for i in range(5): + Environment.objects.create(name=f"Env {i}", project=project) + url = reverse("api-v1:environments:environment-list") + + # When + response = admin_client_new.get( + url, + data={"project": project.id, "page_size": 2}, + content_type="application/json", + ) + + # Then + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert len(data["results"]) == 2 + assert data["count"] >= 5 diff --git a/frontend/common/services/useEnvironment.ts b/frontend/common/services/useEnvironment.ts index 0799aea8c666..40d71733220a 100644 --- a/frontend/common/services/useEnvironment.ts +++ b/frontend/common/services/useEnvironment.ts @@ -1,8 +1,6 @@ -import { FetchBaseQueryError } from '@reduxjs/toolkit/query' -import { Environment, Res } from 'common/types/responses' +import { Res } from 'common/types/responses' import { Req } from 'common/types/requests' import { service } from 'common/service' -import { fetchAllPages } from 'common/utils/fetchAllPages' export const environmentService = service .enhanceEndpoints({ addTagTypes: ['Environment'] }) @@ -19,19 +17,6 @@ export const environmentService = service url: `environments/`, }), }), - getAllEnvironments: builder.query< - Res['allEnvironments'], - Req['getAllEnvironments'] - >({ - providesTags: [{ id: 'LIST', type: 'Environment' }], - queryFn: ({ projectId }, _, _2, baseQuery) => - fetchAllPages(async (page) => { - const { data, error } = await baseQuery({ - url: `environments/?project=${projectId}&page=${page}`, - }) - return { data: data as Res['environments'] | undefined, error } - }), - }), getEnvironment: builder.query({ providesTags: (res) => [{ id: res?.id, type: 'Environment' }], query: (query: Req['getEnvironment']) => ({ @@ -111,7 +96,6 @@ export async function updateEnvironment( export const { useCreateEnvironmentMutation, - useGetAllEnvironmentsQuery, useGetEnvironmentMetricsQuery, useGetEnvironmentQuery, useGetEnvironmentsQuery, diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index a99864c3f2e8..c640d555ef54 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -273,9 +273,6 @@ export type Req = { getEnvironments: { projectId: number } - getAllEnvironments: { - projectId: number - } getOrganisationUsage: { organisationId: number projectId?: number diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index f8dee3a87451..6f769cac03fe 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -1550,7 +1550,6 @@ export type Res = { projects: ProjectSummary[] project: Project environments: PagedResponse - allEnvironments: Environment[] webhook: Webhook webhooks: Webhook[] organisationUsage: { diff --git a/frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts b/frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts index f88434f293ed..baff79d7470e 100644 --- a/frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts +++ b/frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts @@ -3,7 +3,7 @@ import { skipToken } from '@reduxjs/toolkit/query' import { BillingPeriod } from 'common/types/requests' import { useGetOrganisationUsageQuery } from 'common/services/useOrganisationUsage' import { useGetProjectsQuery } from 'common/services/useProject' -import { useGetAllEnvironmentsQuery } from 'common/services/useEnvironment' +import { useGetEnvironmentsQuery } from 'common/services/useEnvironment' import { breakdownStatusOf, retryFailed } from './breakdownView' import { byScope, @@ -50,7 +50,7 @@ export const useGroupedBreakdown = ({ const projects = useGetProjectsQuery( groupBy === 'project' && organisationId ? { organisationId } : skipToken, ) - const environments = useGetAllEnvironmentsQuery( + const environments = useGetEnvironmentsQuery( groupBy === 'environment' && projectId ? { projectId } : skipToken, ) @@ -67,7 +67,7 @@ export const useGroupedBreakdown = ({ return byScope( grouped.currentData, 'environment_id', - new Map(environments.currentData?.map(({ id, name }) => [id, name])), + new Map(environments.currentData?.results?.map(({ id, name }) => [id, name])), 'Deleted environments', ) } From 42c1b3921941e89d7eac9013e1f08516e30ab7ab Mon Sep 17 00:00:00 2001 From: TejasLamba2006 Date: Mon, 9 Nov 2026 22:21:29 +0530 Subject: [PATCH 2/4] test(environments): assert pagination results and pages --- .../test_unit_environments_views.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index c7312a19469e..990145299919 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -1402,8 +1402,8 @@ def test_list_environments__returns_paginated_response( project: Project, ) -> None: # Given - Environment.objects.create(name="Env One", project=project) - Environment.objects.create(name="Env Two", project=project) + env_1 = Environment.objects.create(name="Env One", project=project) + env_2 = Environment.objects.create(name="Env Two", project=project) url = reverse("api-v1:environments:environment-list") # When @@ -1413,8 +1413,12 @@ def test_list_environments__returns_paginated_response( # Then assert response.status_code == status.HTTP_200_OK - assert "results" in response.json() - assert "count" in response.json() + data = response.json() + assert data["count"] == 2 + assert {environment["id"] for environment in data["results"]} == { + env_1.id, + env_2.id, + } def test_list_environments__page_size_param_is_respected( @@ -1429,12 +1433,23 @@ def test_list_environments__page_size_param_is_respected( # When response = admin_client_new.get( url, - data={"project": project.id, "page_size": 2}, + data={"project": project.id, "page_size": 2, "page": 1}, + content_type="application/json", + ) + page_2_response = admin_client_new.get( + url, + data={"project": project.id, "page_size": 2, "page": 2}, content_type="application/json", ) # Then assert response.status_code == status.HTTP_200_OK + assert page_2_response.status_code == status.HTTP_200_OK data = response.json() + page_2_data = page_2_response.json() assert len(data["results"]) == 2 - assert data["count"] >= 5 + assert data["count"] == 5 + page_1_ids = {environment["id"] for environment in data["results"]} + page_2_ids = {environment["id"] for environment in page_2_data["results"]} + assert len(page_2_data["results"]) == 2 + assert page_1_ids.isdisjoint(page_2_ids) From c08c1545a6e3b48da111de59942444b813cc27eb Mon Sep 17 00:00:00 2001 From: TejasLamba2006 Date: Mon, 9 Nov 2026 22:41:03 +0530 Subject: [PATCH 3/4] test: cover environment pagination isolation --- .../test_unit_environments_views.py | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index 990145299919..1bb7130ab8a7 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -1404,6 +1404,11 @@ def test_list_environments__returns_paginated_response( # Given env_1 = Environment.objects.create(name="Env One", project=project) env_2 = Environment.objects.create(name="Env Two", project=project) + env_3 = Environment.objects.create(name="Env Three", project=project) + another_project = Project.objects.create( + name="Other", organisation=project.organisation + ) + Environment.objects.create(name="Other Project Env", project=another_project) url = reverse("api-v1:environments:environment-list") # When @@ -1414,10 +1419,12 @@ def test_list_environments__returns_paginated_response( # Then assert response.status_code == status.HTTP_200_OK data = response.json() - assert data["count"] == 2 + assert data["count"] == 3 + assert len(data["results"]) == 3 assert {environment["id"] for environment in data["results"]} == { env_1.id, env_2.id, + env_3.id, } @@ -1426,8 +1433,13 @@ def test_list_environments__page_size_param_is_respected( project: Project, ) -> None: # Given - for i in range(5): - Environment.objects.create(name=f"Env {i}", project=project) + environments = [ + Environment.objects.create(name=f"Env {i}", project=project) for i in range(5) + ] + another_project = Project.objects.create( + name="Other", organisation=project.organisation + ) + Environment.objects.create(name="Other Project Env", project=another_project) url = reverse("api-v1:environments:environment-list") # When @@ -1441,15 +1453,43 @@ def test_list_environments__page_size_param_is_respected( data={"project": project.id, "page_size": 2, "page": 2}, content_type="application/json", ) + page_3_response = admin_client_new.get( + url, + data={"project": project.id, "page_size": 2, "page": 3}, + content_type="application/json", + ) + page_size_3_response = admin_client_new.get( + url, + data={"project": project.id, "page_size": 3}, + content_type="application/json", + ) # Then assert response.status_code == status.HTTP_200_OK assert page_2_response.status_code == status.HTTP_200_OK + assert page_3_response.status_code == status.HTTP_200_OK + assert page_size_3_response.status_code == status.HTTP_200_OK data = response.json() page_2_data = page_2_response.json() + page_3_data = page_3_response.json() + page_size_3_data = page_size_3_response.json() assert len(data["results"]) == 2 assert data["count"] == 5 + assert data["next"] is not None page_1_ids = {environment["id"] for environment in data["results"]} page_2_ids = {environment["id"] for environment in page_2_data["results"]} + page_3_ids = {environment["id"] for environment in page_3_data["results"]} assert len(page_2_data["results"]) == 2 + assert len(page_3_data["results"]) == 1 + assert page_3_data["next"] is None + assert len(page_size_3_data["results"]) == 3 assert page_1_ids.isdisjoint(page_2_ids) + assert page_1_ids.isdisjoint(page_3_ids) + assert page_2_ids.isdisjoint(page_3_ids) + assert page_1_ids | page_2_ids | page_3_ids == { + environment.id for environment in environments + } + assert page_size_3_data["count"] == 5 + assert {environment["id"] for environment in page_size_3_data["results"]}.issubset( + {environment.id for environment in environments} + ) From 7c850da8924f64155f553914c1ce330b7838ebce Mon Sep 17 00:00:00 2001 From: TejasLamba2006 Date: Mon, 9 Nov 2026 23:07:51 +0530 Subject: [PATCH 4/4] test: cover environment pagination limits --- .../test_unit_environments_views.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index 1bb7130ab8a7..706e0e52449c 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -16,6 +16,7 @@ from rest_framework.test import APIClient from api_keys.models import MasterAPIKey +from app.pagination import CustomPagination from audit.models import AuditLog, RelatedObjectType # type: ignore[attr-defined] from core.constants import STRING from environments.identities.models import Identity @@ -1402,9 +1403,9 @@ def test_list_environments__returns_paginated_response( project: Project, ) -> None: # Given - env_1 = Environment.objects.create(name="Env One", project=project) - env_2 = Environment.objects.create(name="Env Two", project=project) - env_3 = Environment.objects.create(name="Env Three", project=project) + environments = [ + Environment.objects.create(name=f"Env {i}", project=project) for i in range(11) + ] another_project = Project.objects.create( name="Other", organisation=project.organisation ) @@ -1419,23 +1420,24 @@ def test_list_environments__returns_paginated_response( # Then assert response.status_code == status.HTTP_200_OK data = response.json() - assert data["count"] == 3 - assert len(data["results"]) == 3 + assert data["count"] == 11 + assert len(data["results"]) == 11 + assert data["next"] is None assert {environment["id"] for environment in data["results"]} == { - env_1.id, - env_2.id, - env_3.id, + environment.id for environment in environments } def test_list_environments__page_size_param_is_respected( admin_client_new: APIClient, project: Project, + mocker: MockerFixture, ) -> None: # Given environments = [ Environment.objects.create(name=f"Env {i}", project=project) for i in range(5) ] + mocker.patch.object(CustomPagination, "max_page_size", 3) another_project = Project.objects.create( name="Other", organisation=project.organisation ) @@ -1463,16 +1465,23 @@ def test_list_environments__page_size_param_is_respected( data={"project": project.id, "page_size": 3}, content_type="application/json", ) + max_page_size_response = admin_client_new.get( + url, + data={"project": project.id, "page_size": 10}, + content_type="application/json", + ) # Then assert response.status_code == status.HTTP_200_OK assert page_2_response.status_code == status.HTTP_200_OK assert page_3_response.status_code == status.HTTP_200_OK assert page_size_3_response.status_code == status.HTTP_200_OK + assert max_page_size_response.status_code == status.HTTP_200_OK data = response.json() page_2_data = page_2_response.json() page_3_data = page_3_response.json() page_size_3_data = page_size_3_response.json() + max_page_size_data = max_page_size_response.json() assert len(data["results"]) == 2 assert data["count"] == 5 assert data["next"] is not None @@ -1480,9 +1489,14 @@ def test_list_environments__page_size_param_is_respected( page_2_ids = {environment["id"] for environment in page_2_data["results"]} page_3_ids = {environment["id"] for environment in page_3_data["results"]} assert len(page_2_data["results"]) == 2 + assert page_2_data["next"] is not None assert len(page_3_data["results"]) == 1 assert page_3_data["next"] is None - assert len(page_size_3_data["results"]) == 3 + page_size_3_ids = {environment["id"] for environment in page_size_3_data["results"]} + assert len(page_size_3_ids) == 3 + assert page_size_3_ids.issubset({environment.id for environment in environments}) + assert len(max_page_size_data["results"]) == 3 + assert max_page_size_data["next"] is not None assert page_1_ids.isdisjoint(page_2_ids) assert page_1_ids.isdisjoint(page_3_ids) assert page_2_ids.isdisjoint(page_3_ids)