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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/environments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
112 changes: 112 additions & 0 deletions api/tests/unit/environments/test_unit_environments_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1395,3 +1396,114 @@ 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
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
)
Environment.objects.create(name="Other Project Env", project=another_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
data = response.json()
assert data["count"] == 11
assert len(data["results"]) == 11
assert data["next"] is None
Comment on lines +1424 to +1425

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial · Maintainability & Code Quality · Low value

Supported by static analysis

Script executed:

sed -n '1390,1520p' api/tests/unit/environments/test_unit_environments_views.py
cat api/app/pagination.py

Repository: Flagsmith/flagsmith

Length of output: 7849


Script executed:

rg -n -F -e 'CustomPagination' -e 'test_list_environments__' -e 'page_size' api/environments/views.py api/tests/unit/environments/test_unit_environments_views.py api/tests/unit/app/ api/tests/unit/ 2>/dev/null | grep -E 'environments/views.py|environments/test_unit_environments_views.py|pagination' 
nl -ba api/tests/unit/environments/test_unit_environments_views.py | sed -n '1400,1510p'
nl -ba api/app/pagination.py | sed -n '1,22p'

Repository: Flagsmith/flagsmith

Length of output: 12255


Script executed:

rg -n -F -e 'CustomPagination' -e 'test_list_environments__' -e 'page_size' api/environments/views.py api/tests/unit/environments/test_unit_environments_views.py api/tests/unit/app/ api/tests/unit/ 2>/dev/null | grep -E 'environments/views.py|environments/test_unit_environments_views.py|pagination'
nl -ba api/tests/unit/environments/test_unit_environments_views.py | sed -n '1400,1510p'
nl -ba api/app/pagination.py | sed -n '1,22p'

Repository: Flagsmith/flagsmith

Length of output: 12255


Cover the 999-result default boundary.

The current test detects the former 10-result default, but not a change from 999 to 11. Consider a separate test with 1,000 environments that checks for 999 results and a next page.

assert {environment["id"] for environment in data["results"]} == {
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial · Functional Correctness · Low value

Supported by static analysis

Script executed:

sed -n '1430,1520p' api/tests/unit/environments/test_unit_environments_views.py
rg -n 'max_page_size|10000|999' api/tests/unit/app api/tests/unit/environments api/app/pagination.py

Repository: Flagsmith/flagsmith

Length of output: 5542


Script executed:

printf '%s\n' '--- Pagination and environment list test ---'
nl -ba api/app/pagination.py | sed -n '1,40p'
nl -ba api/environments/views.py | sed -n '55,115p'
nl -ba api/tests/unit/environments/test_unit_environments_views.py | sed -n '1385,1508p'
printf '%s\n' '--- Environment model location ---'
rg -n 'class Environment\(|def save\(|post_save|pre_save|Environment\.objects\.bulk_create' api/environments api/tests/unit/environments --glob '*.py' | sed -n '1,110p'

Repository: Flagsmith/flagsmith

Length of output: 10404


Script executed:

printf '%s\n' '--- Environment model and creation hooks ---'
nl -ba api/environments/models.py | sed -n '65,230p'
rg -n -F -e 'sender=Environment' -e 'sender = Environment' -e 'post_save.connect' -e 'pre_save.connect' api/environments api/features api/projects --glob '*.py' || test "$?" -eq 1
printf '%s\n' '--- Edge identity limit test ---'
nl -ba api/tests/integration/edge_api/identities/test_edge_identity_viewset.py | sed -n '220,265p'

Repository: Flagsmith/flagsmith

Length of output: 11432


Test the configured page-size limit.

The patched maximum tests capping but does not protect the configured limit of 999. Add an unpatched test with more than 999 environments and page_size=10000. Assert that the response contains 999 results and a next page.

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
response = admin_client_new.get(
url,
data={"project": project.id, "page_size": 2, "page": 1},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
content_type="application/json",
)
page_2_response = admin_client_new.get(
url,
data={"project": project.id, "page_size": 2, "page": 2},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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
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 page_2_data["next"] is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial · Functional Correctness · Low value

Supported by static analysis

Script executed:

sed -n '1430,1520p' api/tests/unit/environments/test_unit_environments_views.py
rg -n 'next.*]|next.*link|page_2_data' api/tests/unit/environments

Repository: Flagsmith/flagsmith

Length of output: 4379


Script executed:

rg -n -F -e 'CustomPagination' -e '["next"]' -e "['next']" api/tests/unit/environments api/tests/integration 2>/dev/null; printf '\n--- identity pagination test ---\n'; nl -ba api/tests/unit/environments/identities/test_unit_identities_views.py | sed -n '255,320p'; printf '\n--- pagination implementation ---\n'; nl -ba api/app/pagination.py | sed -n '1,100p'; printf '\n--- endpoint ---\n'; nl -ba api/environments/views.py | sed -n '65,135p'; printf '\n--- changed test ---\n'; nl -ba api/tests/unit/environments/test_unit_environments_views.py | sed -n '1400,1515p'

Repository: Flagsmith/flagsmith

Length of output: 15262


Follow the page-two link in the test.

The test requests page three independently, so it accepts a next link that omits parameters or points back to page two. Follow page_2_data["next"] to check that the link returns the final environment.

Suggested change

-    page_3_response = admin_client_new.get(
-        url,
-        data={"project": project.id, "page_size": 2, "page": 3},
-        content_type="application/json",
-    )
+    page_3_response = admin_client_new.get(page_2_response.json()["next"])

assert len(page_3_data["results"]) == 1
assert page_3_data["next"] is None
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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)
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}
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
18 changes: 1 addition & 17 deletions frontend/common/services/useEnvironment.ts
Original file line number Diff line number Diff line change
@@ -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'] })
Expand All @@ -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<Environment, FetchBaseQueryError>(async (page) => {
const { data, error } = await baseQuery({
url: `environments/?project=${projectId}&page=${page}`,
})
return { data: data as Res['environments'] | undefined, error }
}),
}),
getEnvironment: builder.query<Res['environment'], Req['getEnvironment']>({
providesTags: (res) => [{ id: res?.id, type: 'Environment' }],
query: (query: Req['getEnvironment']) => ({
Expand Down Expand Up @@ -111,7 +96,6 @@ export async function updateEnvironment(

export const {
useCreateEnvironmentMutation,
useGetAllEnvironmentsQuery,
useGetEnvironmentMetricsQuery,
useGetEnvironmentQuery,
useGetEnvironmentsQuery,
Expand Down
3 changes: 0 additions & 3 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,6 @@ export type Req = {
getEnvironments: {
projectId: number
}
getAllEnvironments: {
projectId: number
}
getOrganisationUsage: {
organisationId: number
projectId?: number
Expand Down
1 change: 0 additions & 1 deletion frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1550,7 +1550,6 @@ export type Res = {
projects: ProjectSummary[]
project: Project
environments: PagedResponse<Environment>
allEnvironments: Environment[]
webhook: Webhook
webhooks: Webhook[]
organisationUsage: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Comment on lines +53 to 54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor · Functional Correctness · Low value

Supported by static analysis

Script executed:

rg -n 'function byScope|const byScope|export .*byScope|fetchAllPages|useGetAllEnvironmentsQuery|useGetEnvironmentsQuery|group_by.*environment' frontend/web/components/pages/usage frontend/common/services frontend/common/utils | head -100

Repository: Flagsmith/flagsmith

Length of output: 1411


Script executed:

git diff 13539afcb99c20b4351e26f215d01b49f2a9be03 d5e8676c5717fcf26498f7c4c77b2226beef31df -- frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts frontend/common/services/useEnvironment.ts api/environments/views.py
nl -ba frontend/web/components/pages/usage/components/UsageBreakdown/useGroupedBreakdown.ts
nl -ba frontend/web/components/pages/usage/components/UsageBreakdown/utils.ts | sed -n '1,220p'
nl -ba frontend/common/services/useEnvironment.ts | sed -n '1,140p'
nl -ba frontend/common/utils/fetchAllPages.ts
rg -n -i 'organisation.?usage|organization.?usage|group_by|groupBy' frontend/web/components/pages/usage frontend/common/services api/organisations api/usage 2>/dev/null

Repository: Flagsmith/flagsmith

Length of output: 25426


Script executed:

nl -ba frontend/common/services/useOrganisationUsage.ts | sed -n '1,80p'
nl -ba api/app/pagination.py
nl -ba api/environments/views.py | sed -n '80,220p'
rg -n -i -e 'group_by' -e 'environment_id' -- api | grep -i -E 'usage|analytics|influx|event' 
rg -n -i -e 'environment.limit' -e 'max.environments' -e 'environments.limit' -e 'maximum.number.of.environments' -- api frontend | grep -v -E 'lock|snapshot'

Repository: Flagsmith/flagsmith

Length of output: 48186


Script executed:

rg -n -F -e 'get_usage_data' -e 'usage-data' -- api/app_analytics api/organisations
rg -n -F -e 'class CreateEnvironmentSerializer' -e 'class Environment(' -- api/environments
nl -ba api/app_analytics/analytics_db_service.py | sed -n '35,190p'
nl -ba api/app_analytics/analytics_db_service.py | sed -n '355,400p'
nl -ba api/tests/unit/app_analytics/test_analytics_db_service.py | sed -n '960,1125p'
nl -ba api/tests/unit/app_analytics/test_unit_app_analytics_views.py | sed -n '460,492p'
rg -n -i -e 'environments_per' -e 'environment_count' -e 'environment_limit' -e 'environments_limit' -e 'max_environments' -- api | grep -v -E 'migrations|tests/unit/integrations' || test "$?" -eq 1

Repository: Flagsmith/flagsmith

Length of output: 19773


Script executed:

nl -ba api/app_analytics/views.py | sed -n '1,135p'
nl -ba api/environments/serializers.py | sed -n '1,200p'
nl -ba api/environments/models.py | sed -n '75,180p'
nl -ba frontend/web/components/pages/usage/components/UsageBreakdown/UsageBreakdown.tsx
nl -ba frontend/web/components/pages/usage/components/UsageBreakdown/breakdownView.ts | sed -n '1,120p'

Repository: Flagsmith/flagsmith

Length of output: 22769


Fetch every environment page for the usage breakdown.

When a project has more than 999 permitted environments, usage for an environment on a later page can appear under Deleted environments. Fetch all environment pages before building the name lookup.

)

Expand All @@ -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',
)
}
Expand Down