From 8492d3631cf0d17c240aba55d15063843f31dcb5 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Apr 2026 17:57:20 +0200 Subject: [PATCH 1/9] feat: display-saml-tab-for-scale-up-plans --- frontend/common/utils/utils.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/common/utils/utils.tsx b/frontend/common/utils/utils.tsx index 61d66b67c00f..450ed65d1222 100644 --- a/frontend/common/utils/utils.tsx +++ b/frontend/common/utils/utils.tsx @@ -517,15 +517,15 @@ const Utils = Object.assign({}, BaseUtils, { case 'RBAC': case 'AUDIT': case '4_EYES_PROJECT': - case '4_EYES': { + case '4_EYES': + case 'SAML': { plan = 'scale-up' break } case 'STALE_FLAGS': case 'REALTIME': case 'METADATA': - case 'RELEASE_PIPELINES': - case 'SAML': { + case 'RELEASE_PIPELINES': { plan = 'enterprise' break } From f32e79877f871342e7f18724058082c33726b07c Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Apr 2026 17:57:52 +0200 Subject: [PATCH 2/9] feat: backend-verifying-plan-permissions-for-saml-endpoints --- api/app/urls.py | 8 + .../subscriptions/permissions.py | 65 +++++ .../test_unit_subscriptions_permissions.py | 243 ++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 api/organisations/subscriptions/permissions.py create mode 100644 api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py diff --git a/api/app/urls.py b/api/app/urls.py index 253a9e8e6e66..27e2f2111e1f 100644 --- a/api/app/urls.py +++ b/api/app/urls.py @@ -97,6 +97,14 @@ ] + urlpatterns if settings.SAML_INSTALLED: # pragma: no cover + from saml.views import SamlConfigurationViewSet + + from organisations.subscriptions.constants import SubscriptionPlanFamily + from organisations.subscriptions.permissions import require_minimum_plan + + scale_up_permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP) + SamlConfigurationViewSet.permission_classes += [scale_up_permission] + urlpatterns += [ path("api/v1/auth/saml/", include("saml.urls")), ] diff --git a/api/organisations/subscriptions/permissions.py b/api/organisations/subscriptions/permissions.py new file mode 100644 index 000000000000..e025cb066750 --- /dev/null +++ b/api/organisations/subscriptions/permissions.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from common.core.utils import is_saas +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import APIView + +from organisations.models import Organisation +from organisations.subscriptions.constants import SubscriptionPlanFamily + +_PLAN_RANK = { + SubscriptionPlanFamily.FREE: 0, + SubscriptionPlanFamily.START_UP: 1, + SubscriptionPlanFamily.SCALE_UP: 2, + SubscriptionPlanFamily.ENTERPRISE: 3, +} + + +def require_minimum_plan(minimum: SubscriptionPlanFamily) -> type[BasePermission]: + """ + Return a DRF permission class that requires the organisation associated + with the request to be on `minimum` plan family or higher. + + On non-SaaS deployments (self-hosted OSS / Enterprise), plan gating is + always bypassed. These deployments don't use Chargebee subscriptions — + entitlements are handled via the enterprise licence file instead, so + `Subscription.plan` is typically `"free"` and not meaningful. + + On SaaS, the organisation is read from: + - `obj.organisation` for detail actions (via `has_object_permission`) + - `request.data["organisation"]` or `?organisation=` for list/create + """ + min_rank = _PLAN_RANK[minimum] + + def _meets(org: Organisation) -> bool: + sub = getattr(org, "subscription", None) + if sub is None: + return False + return _PLAN_RANK.get(sub.subscription_plan_family, -1) >= min_rank + + class _MinimumPlanPermission(BasePermission): + message = f"This resource requires a {minimum.value} plan or above." + + def has_permission(self, request: Request, view: APIView) -> bool: + if not is_saas(): + return True + org_id = request.data.get("organisation") or request.query_params.get( + "organisation" + ) + if not org_id: + # defer to has_object_permission for detail actions; + # list/create without an org will be caught by the view's validation + return True + org = Organisation.objects.filter(id=org_id).first() + return org is not None and _meets(org) + + def has_object_permission( + self, request: Request, view: APIView, obj: object + ) -> bool: + if not is_saas(): + return True + org = getattr(obj, "organisation", None) + return isinstance(org, Organisation) and _meets(org) + + return _MinimumPlanPermission diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py new file mode 100644 index 000000000000..474a25d39701 --- /dev/null +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -0,0 +1,243 @@ +from unittest.mock import MagicMock + +import pytest +from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] +from pytest_mock import MockerFixture +from rest_framework.request import Request + +from organisations.models import Organisation, Subscription +from organisations.subscriptions.constants import SubscriptionPlanFamily +from organisations.subscriptions.permissions import require_minimum_plan + + +@pytest.fixture +def saas(mocker: MockerFixture) -> None: + mocker.patch( + "organisations.subscriptions.permissions.is_saas", return_value=True + ) + + +@pytest.fixture +def self_hosted(mocker: MockerFixture) -> None: + mocker.patch( + "organisations.subscriptions.permissions.is_saas", return_value=False + ) + + +@pytest.mark.parametrize( + "subscription_fixture, expected", + [ + (lazy_fixture("free_subscription"), False), + (lazy_fixture("startup_subscription"), False), + (lazy_fixture("scale_up_subscription"), True), + (lazy_fixture("enterprise_subscription"), True), + ], +) +def test_require_minimum_plan__has_permission__plan_matrix( + saas: None, + organisation: Organisation, + subscription_fixture: Subscription, + expected: bool, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {"organisation": organisation.id} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is expected + + +@pytest.mark.parametrize( + "subscription_fixture, expected", + [ + (lazy_fixture("free_subscription"), False), + (lazy_fixture("startup_subscription"), False), + (lazy_fixture("scale_up_subscription"), True), + (lazy_fixture("enterprise_subscription"), True), + ], +) +def test_require_minimum_plan__has_object_permission__plan_matrix( + saas: None, + organisation: Organisation, + subscription_fixture: Subscription, + expected: bool, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + obj = MagicMock() + obj.organisation = organisation + + # When / Then + assert ( + permission.has_object_permission(MagicMock(spec=Request), MagicMock(), obj) + is expected + ) + + +@pytest.mark.parametrize("source", ["data", "query_params"]) +def test_require_minimum_plan__organisation_lookup__reads_from_data_and_query_params( + saas: None, + organisation: Organisation, + free_subscription: Subscription, + source: str, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {} + request.query_params = {} + setattr(request, source, {"organisation": organisation.id}) + + # When / Then + assert permission.has_permission(request, MagicMock()) is False + + +def test_require_minimum_plan__no_organisation_in_request__defers_to_object_level( + saas: None, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is True + + +def test_require_minimum_plan__unknown_organisation_id__returns_false( + saas: None, + db: None, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {"organisation": 999999} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is False + + +def test_require_minimum_plan__has_object_permission__no_organisation_attr__returns_false( + saas: None, +) -> None: + # Given + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + + # When / Then + assert ( + permission.has_object_permission( + MagicMock(spec=Request), MagicMock(), object() + ) + is False + ) + + +@pytest.mark.parametrize( + "minimum, subscription_fixture, allowed", + [ + (SubscriptionPlanFamily.START_UP, lazy_fixture("free_subscription"), False), + (SubscriptionPlanFamily.START_UP, lazy_fixture("startup_subscription"), True), + (SubscriptionPlanFamily.START_UP, lazy_fixture("scale_up_subscription"), True), + ( + SubscriptionPlanFamily.START_UP, + lazy_fixture("enterprise_subscription"), + True, + ), + (SubscriptionPlanFamily.SCALE_UP, lazy_fixture("startup_subscription"), False), + (SubscriptionPlanFamily.SCALE_UP, lazy_fixture("scale_up_subscription"), True), + ( + SubscriptionPlanFamily.SCALE_UP, + lazy_fixture("enterprise_subscription"), + True, + ), + ( + SubscriptionPlanFamily.ENTERPRISE, + lazy_fixture("scale_up_subscription"), + False, + ), + ( + SubscriptionPlanFamily.ENTERPRISE, + lazy_fixture("enterprise_subscription"), + True, + ), + ], +) +def test_require_minimum_plan__plan_hierarchy__honours_ordering( + saas: None, + organisation: Organisation, + minimum: SubscriptionPlanFamily, + subscription_fixture: Subscription, + allowed: bool, +) -> None: + # Given + permission = require_minimum_plan(minimum)() + request = MagicMock(spec=Request) + request.data = {"organisation": organisation.id} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is allowed + + +@pytest.mark.parametrize( + "plan_id", + [ + "scale-up-v2", + "scale-up-monthly", + "enterprise-saas-monthly-v2", # used for trials + ], +) +def test_require_minimum_plan__real_world_plan_ids__allowed( + saas: None, + organisation: Organisation, + plan_id: str, +) -> None: + # Given + Subscription.objects.filter(organisation=organisation).update( + plan=plan_id, subscription_id="subscription-id" + ) + organisation.refresh_from_db() + + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {"organisation": organisation.id} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is True + + +def test_require_minimum_plan__self_hosted__has_permission__bypasses_check( + self_hosted: None, + organisation: Organisation, + free_subscription: Subscription, +) -> None: + # Given a self-hosted deployment, even a free-plan organisation is permitted. + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + request = MagicMock(spec=Request) + request.data = {"organisation": organisation.id} + request.query_params = {} + + # When / Then + assert permission.has_permission(request, MagicMock()) is True + + +def test_require_minimum_plan__self_hosted__has_object_permission__bypasses_check( + self_hosted: None, + organisation: Organisation, + free_subscription: Subscription, +) -> None: + # Given a self-hosted deployment, object-level checks are also bypassed. + permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() + obj = MagicMock() + obj.organisation = organisation + + # When / Then + assert ( + permission.has_object_permission(MagicMock(spec=Request), MagicMock(), obj) + is True + ) From 33e132691fb3a9a906559f0f0fde8636d62bdc06 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:11:54 +0000 Subject: [PATCH 3/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../test_unit_subscriptions_permissions.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py index 474a25d39701..1cbe98b5b843 100644 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -12,16 +12,12 @@ @pytest.fixture def saas(mocker: MockerFixture) -> None: - mocker.patch( - "organisations.subscriptions.permissions.is_saas", return_value=True - ) + mocker.patch("organisations.subscriptions.permissions.is_saas", return_value=True) @pytest.fixture def self_hosted(mocker: MockerFixture) -> None: - mocker.patch( - "organisations.subscriptions.permissions.is_saas", return_value=False - ) + mocker.patch("organisations.subscriptions.permissions.is_saas", return_value=False) @pytest.mark.parametrize( @@ -129,9 +125,7 @@ def test_require_minimum_plan__has_object_permission__no_organisation_attr__retu # When / Then assert ( - permission.has_object_permission( - MagicMock(spec=Request), MagicMock(), object() - ) + permission.has_object_permission(MagicMock(spec=Request), MagicMock(), object()) is False ) From f5cfb17f506ecd770c99f762d2f974d73fd57899 Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 20 Apr 2026 11:15:34 +0200 Subject: [PATCH 4/9] feat: linted tests and reduced redundancy --- .../subscriptions/permissions.py | 5 +- .../test_unit_subscriptions_permissions.py | 58 ++----------------- 2 files changed, 5 insertions(+), 58 deletions(-) diff --git a/api/organisations/subscriptions/permissions.py b/api/organisations/subscriptions/permissions.py index e025cb066750..7100928a1221 100644 --- a/api/organisations/subscriptions/permissions.py +++ b/api/organisations/subscriptions/permissions.py @@ -33,10 +33,7 @@ def require_minimum_plan(minimum: SubscriptionPlanFamily) -> type[BasePermission min_rank = _PLAN_RANK[minimum] def _meets(org: Organisation) -> bool: - sub = getattr(org, "subscription", None) - if sub is None: - return False - return _PLAN_RANK.get(sub.subscription_plan_family, -1) >= min_rank + return _PLAN_RANK.get(org.subscription.subscription_plan_family, -1) >= min_rank class _MinimumPlanPermission(BasePermission): message = f"This resource requires a {minimum.value} plan or above." diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py index 474a25d39701..135900cdf950 100644 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -121,7 +121,7 @@ def test_require_minimum_plan__unknown_organisation_id__returns_false( assert permission.has_permission(request, MagicMock()) is False -def test_require_minimum_plan__has_object_permission__no_organisation_attr__returns_false( +def test_require_minimum_plan__has_object_permission__obj_without_organisation__returns_false( saas: None, ) -> None: # Given @@ -141,19 +141,8 @@ def test_require_minimum_plan__has_object_permission__no_organisation_attr__retu [ (SubscriptionPlanFamily.START_UP, lazy_fixture("free_subscription"), False), (SubscriptionPlanFamily.START_UP, lazy_fixture("startup_subscription"), True), - (SubscriptionPlanFamily.START_UP, lazy_fixture("scale_up_subscription"), True), - ( - SubscriptionPlanFamily.START_UP, - lazy_fixture("enterprise_subscription"), - True, - ), (SubscriptionPlanFamily.SCALE_UP, lazy_fixture("startup_subscription"), False), (SubscriptionPlanFamily.SCALE_UP, lazy_fixture("scale_up_subscription"), True), - ( - SubscriptionPlanFamily.SCALE_UP, - lazy_fixture("enterprise_subscription"), - True, - ), ( SubscriptionPlanFamily.ENTERPRISE, lazy_fixture("scale_up_subscription"), @@ -183,60 +172,21 @@ def test_require_minimum_plan__plan_hierarchy__honours_ordering( assert permission.has_permission(request, MagicMock()) is allowed -@pytest.mark.parametrize( - "plan_id", - [ - "scale-up-v2", - "scale-up-monthly", - "enterprise-saas-monthly-v2", # used for trials - ], -) -def test_require_minimum_plan__real_world_plan_ids__allowed( - saas: None, - organisation: Organisation, - plan_id: str, -) -> None: - # Given - Subscription.objects.filter(organisation=organisation).update( - plan=plan_id, subscription_id="subscription-id" - ) - organisation.refresh_from_db() - - permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() - request = MagicMock(spec=Request) - request.data = {"organisation": organisation.id} - request.query_params = {} - - # When / Then - assert permission.has_permission(request, MagicMock()) is True - - -def test_require_minimum_plan__self_hosted__has_permission__bypasses_check( +def test_require_minimum_plan__self_hosted__bypasses_check( self_hosted: None, organisation: Organisation, free_subscription: Subscription, ) -> None: - # Given a self-hosted deployment, even a free-plan organisation is permitted. + # Given permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() request = MagicMock(spec=Request) request.data = {"organisation": organisation.id} request.query_params = {} - - # When / Then - assert permission.has_permission(request, MagicMock()) is True - - -def test_require_minimum_plan__self_hosted__has_object_permission__bypasses_check( - self_hosted: None, - organisation: Organisation, - free_subscription: Subscription, -) -> None: - # Given a self-hosted deployment, object-level checks are also bypassed. - permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() obj = MagicMock() obj.organisation = organisation # When / Then + assert permission.has_permission(request, MagicMock()) is True assert ( permission.has_object_permission(MagicMock(spec=Request), MagicMock(), obj) is True From 573c09f868fdb2bb1172a7148d56bb571739975f Mon Sep 17 00:00:00 2001 From: wadii Date: Mon, 20 Apr 2026 11:55:42 +0200 Subject: [PATCH 5/9] feat: lint test names --- .../subscriptions/test_unit_subscriptions_permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py index e20caf673752..55db1bc31819 100644 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -117,7 +117,7 @@ def test_require_minimum_plan__unknown_organisation_id__returns_false( assert permission.has_permission(request, MagicMock()) is False -def test_require_minimum_plan__has_object_permission__obj_without_organisation__returns_false( +def test_require_minimum_plan_has_object_permission__obj_without_organisation__returns_false( saas: None, ) -> None: # Given From 082580f9dd4ac720d5b405e73cb926875882cd5f Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 21 Apr 2026 11:25:07 +0200 Subject: [PATCH 6/9] fix: removed unused future --- api/organisations/subscriptions/permissions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/organisations/subscriptions/permissions.py b/api/organisations/subscriptions/permissions.py index 7100928a1221..ca421d0fa3cc 100644 --- a/api/organisations/subscriptions/permissions.py +++ b/api/organisations/subscriptions/permissions.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from common.core.utils import is_saas from rest_framework.permissions import BasePermission from rest_framework.request import Request From e68e172505bfdc5594cb1dbfa5f01754704163b7 Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 21 Apr 2026 11:26:09 +0200 Subject: [PATCH 7/9] fix: removed unused minimum plan decorator --- api/organisations/subscriptions/decorators.py | 38 ----------------- .../test_unit_subscriptions_decorators.py | 42 ------------------- 2 files changed, 80 deletions(-) delete mode 100644 api/organisations/subscriptions/decorators.py delete mode 100644 api/tests/unit/organisations/subscriptions/test_unit_subscriptions_decorators.py diff --git a/api/organisations/subscriptions/decorators.py b/api/organisations/subscriptions/decorators.py deleted file mode 100644 index 8ec7524e4291..000000000000 --- a/api/organisations/subscriptions/decorators.py +++ /dev/null @@ -1,38 +0,0 @@ -import typing - -from rest_framework.request import Request - -from organisations.models import Subscription -from organisations.subscriptions.exceptions import InvalidSubscriptionPlanError - - -def require_plan( # type: ignore[no-untyped-def] - valid_plan_ids: typing.Iterable[str], - subscription_retriever: typing.Callable[[Request], Subscription], -): - """ - Decorator to be used on view functions / methods that require a specific plan. - - Will result in 403 if resource requested is not part of an organisation that has - the correct subscription. - """ - - def decorator(func): # type: ignore[no-untyped-def] - def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] - if not (args and isinstance(args[0], Request)): - raise ValueError( - "require_plan decorator must be used on methods / functions whose " - "first argument is a Request object." - ) - - sub = subscription_retriever(args[0]) - if not sub or sub.plan not in valid_plan_ids: # type: ignore[operator] - raise InvalidSubscriptionPlanError( - f"Resource not available on plan {sub.plan}" - ) - - return func(*args, **kwargs) - - return wrapper - - return decorator diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_decorators.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_decorators.py deleted file mode 100644 index 97bedfb1bf88..000000000000 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_decorators.py +++ /dev/null @@ -1,42 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -from rest_framework.request import Request - -from organisations.subscriptions.decorators import require_plan -from organisations.subscriptions.exceptions import InvalidSubscriptionPlanError - - -def test_require_plan__invalid_plan__raises_exception(): # type: ignore[no-untyped-def] - # Given - valid_plan_id = "plan-id" - invalid_plan_id = "invalid-plan-id" - - mock_request = MagicMock(spec=Request) - mock_subscription = MagicMock(plan=invalid_plan_id) - - @require_plan([valid_plan_id], lambda v: mock_subscription) # type: ignore[misc] - def test_function(request: Request): # type: ignore[no-untyped-def] - return "foo" - - # When / Then - with pytest.raises(InvalidSubscriptionPlanError): - test_function(mock_request) - - -def test_require_plan__valid_plan__returns_function_result(rf): # type: ignore[no-untyped-def] - # Given - valid_plan_id = "plan-id" - - mock_request = MagicMock(spec=Request) - mock_subscription = MagicMock(plan=valid_plan_id) - - @require_plan([valid_plan_id], lambda v: mock_subscription) # type: ignore[misc] - def test_function(request: Request): # type: ignore[no-untyped-def] - return "foo" - - # When - res = test_function(mock_request) - - # Then - assert res == "foo" From 58b77a6976be1c7e70c0bf8758f85fdd9f10544d Mon Sep 17 00:00:00 2001 From: wadii Date: Tue, 21 Apr 2026 11:45:47 +0200 Subject: [PATCH 8/9] fix: removed redundant fixtures --- .../test_unit_subscriptions_permissions.py | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py index 55db1bc31819..26f4b9382815 100644 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -2,7 +2,6 @@ import pytest from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] -from pytest_mock import MockerFixture from rest_framework.request import Request from organisations.models import Organisation, Subscription @@ -10,16 +9,7 @@ from organisations.subscriptions.permissions import require_minimum_plan -@pytest.fixture -def saas(mocker: MockerFixture) -> None: - mocker.patch("organisations.subscriptions.permissions.is_saas", return_value=True) - - -@pytest.fixture -def self_hosted(mocker: MockerFixture) -> None: - mocker.patch("organisations.subscriptions.permissions.is_saas", return_value=False) - - +@pytest.mark.saas_mode @pytest.mark.parametrize( "subscription_fixture, expected", [ @@ -30,7 +20,6 @@ def self_hosted(mocker: MockerFixture) -> None: ], ) def test_require_minimum_plan__has_permission__plan_matrix( - saas: None, organisation: Organisation, subscription_fixture: Subscription, expected: bool, @@ -45,6 +34,7 @@ def test_require_minimum_plan__has_permission__plan_matrix( assert permission.has_permission(request, MagicMock()) is expected +@pytest.mark.saas_mode @pytest.mark.parametrize( "subscription_fixture, expected", [ @@ -55,7 +45,6 @@ def test_require_minimum_plan__has_permission__plan_matrix( ], ) def test_require_minimum_plan__has_object_permission__plan_matrix( - saas: None, organisation: Organisation, subscription_fixture: Subscription, expected: bool, @@ -72,9 +61,9 @@ def test_require_minimum_plan__has_object_permission__plan_matrix( ) +@pytest.mark.saas_mode @pytest.mark.parametrize("source", ["data", "query_params"]) def test_require_minimum_plan__organisation_lookup__reads_from_data_and_query_params( - saas: None, organisation: Organisation, free_subscription: Subscription, source: str, @@ -90,9 +79,8 @@ def test_require_minimum_plan__organisation_lookup__reads_from_data_and_query_pa assert permission.has_permission(request, MagicMock()) is False -def test_require_minimum_plan__no_organisation_in_request__defers_to_object_level( - saas: None, -) -> None: +@pytest.mark.saas_mode +def test_require_minimum_plan__no_organisation_in_request__defers_to_object_level() -> None: # Given permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() request = MagicMock(spec=Request) @@ -103,8 +91,8 @@ def test_require_minimum_plan__no_organisation_in_request__defers_to_object_leve assert permission.has_permission(request, MagicMock()) is True +@pytest.mark.saas_mode def test_require_minimum_plan__unknown_organisation_id__returns_false( - saas: None, db: None, ) -> None: # Given @@ -117,9 +105,8 @@ def test_require_minimum_plan__unknown_organisation_id__returns_false( assert permission.has_permission(request, MagicMock()) is False -def test_require_minimum_plan_has_object_permission__obj_without_organisation__returns_false( - saas: None, -) -> None: +@pytest.mark.saas_mode +def test_require_minimum_plan_has_object_permission__obj_without_organisation__returns_false() -> None: # Given permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() @@ -130,6 +117,7 @@ def test_require_minimum_plan_has_object_permission__obj_without_organisation__r ) +@pytest.mark.saas_mode @pytest.mark.parametrize( "minimum, subscription_fixture, allowed", [ @@ -150,7 +138,6 @@ def test_require_minimum_plan_has_object_permission__obj_without_organisation__r ], ) def test_require_minimum_plan__plan_hierarchy__honours_ordering( - saas: None, organisation: Organisation, minimum: SubscriptionPlanFamily, subscription_fixture: Subscription, @@ -167,7 +154,6 @@ def test_require_minimum_plan__plan_hierarchy__honours_ordering( def test_require_minimum_plan__self_hosted__bypasses_check( - self_hosted: None, organisation: Organisation, free_subscription: Subscription, ) -> None: From 046bbb8e48a3ce5022ebdf5ef0698dfb9b400d70 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:46:04 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../subscriptions/test_unit_subscriptions_permissions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py index 26f4b9382815..166bf90bf47e 100644 --- a/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py +++ b/api/tests/unit/organisations/subscriptions/test_unit_subscriptions_permissions.py @@ -80,7 +80,9 @@ def test_require_minimum_plan__organisation_lookup__reads_from_data_and_query_pa @pytest.mark.saas_mode -def test_require_minimum_plan__no_organisation_in_request__defers_to_object_level() -> None: +def test_require_minimum_plan__no_organisation_in_request__defers_to_object_level() -> ( + None +): # Given permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)() request = MagicMock(spec=Request) @@ -106,7 +108,9 @@ def test_require_minimum_plan__unknown_organisation_id__returns_false( @pytest.mark.saas_mode -def test_require_minimum_plan_has_object_permission__obj_without_organisation__returns_false() -> None: +def test_require_minimum_plan_has_object_permission__obj_without_organisation__returns_false() -> ( + None +): # Given permission = require_minimum_plan(SubscriptionPlanFamily.SCALE_UP)()