From 0abb41662052e9ab804e9df619a856f1be6cfd00 Mon Sep 17 00:00:00 2001 From: Jon Date: Sun, 1 Jun 2025 07:16:04 +0930 Subject: [PATCH] test(api): API Permissions Auto-Creator test suite ref: #780 #730 #767 --- .../test_functional_meta_permissions_api.py | 165 ++++++++++++++++++ .../test_functional_git_group_permission.py | 27 --- .../test_unit_feature_flag_model.py | 35 +--- .../test_software_item_ticket_api_v2.py | 2 +- app/tests/fixtures/__init__.py | 9 + app/tests/fixtures/kwargs_api_create.py | 2 + app/tests/fixtures/model_centurionmodel.py | 2 +- app/tests/fixtures/model_featureflag.py | 42 +++-- app/tests/fixtures/model_instance.py | 5 +- app/tests/fixtures/model_software.py | 24 +++ .../model_softwareenablefeatureflag.py | 10 ++ .../centurion_erp/development/models.md | 17 +- pyproject.toml | 1 + 13 files changed, 261 insertions(+), 80 deletions(-) create mode 100644 app/api/tests/functional/test_functional_meta_permissions_api.py delete mode 100644 app/devops/tests/functional/git_group/test_functional_git_group_permission.py create mode 100644 app/tests/fixtures/model_software.py create mode 100644 app/tests/fixtures/model_softwareenablefeatureflag.py diff --git a/app/api/tests/functional/test_functional_meta_permissions_api.py b/app/api/tests/functional/test_functional_meta_permissions_api.py new file mode 100644 index 00000000..3f7045da --- /dev/null +++ b/app/api/tests/functional/test_functional_meta_permissions_api.py @@ -0,0 +1,165 @@ +import pytest + +from django.apps import apps +from django.conf import settings + +from api.tests.functional.test_functional_permissions_api import ( + APIPermissionsInheritedCases +) + +from core.models.centurion import CenturionModel + + + +def get_models( excludes: list[ str ] = [] ) -> list[ tuple ]: + """Fetch models from Centurion Apps + + Args: + excludes (list[ str ]): Words that may be in a models name to exclude + + Returns: + list[ tuple ]: Centurion ERP Only models + """ + + models: list = [] + + model_apps: list = [] + + exclude_model_apps = [ + 'django', + 'django_celery_results', + 'django_filters', + 'drf_spectacular', + 'drf_spectacular_sidecar', + 'coresheaders', + 'corsheaders', + 'rest_framework', + 'rest_framework_json_api', + 'social_django', + ] + + for app in settings.INSTALLED_APPS: + + app = app.split('.')[0] + + if app in exclude_model_apps: + continue + + model_apps += [ app ] + + + for model in apps.get_models(): + + if model._meta.app_label not in model_apps: + continue + + skip = False + + for exclude in excludes: + + if exclude in str(model._meta.model_name): + skip = True + break + + if skip: + continue + + models += [ model ] + + return models + + +def make_fixture_with_args(arg_names, func, decorator_factory=None, decorator_args=None): + args_str = ", ".join(arg_names) + + src = f""" +@decorator_factory(**decorator_args) +def _generated(self, {args_str}): + yield from func(self, {args_str}) +""" + + local_ns = {} + global_ns = { + "func": func, + "decorator_factory": decorator_factory, + "decorator_args": decorator_args, + } + + exec(src, global_ns, local_ns) + return local_ns["_generated"] + + + + + +def model(self, model__model_name): + + yield model__model_name + + +def model_kwargs(self, request, kwargs__model_name): + + request.cls.kwargs_create_item = kwargs__model_name.copy() + + yield kwargs__model_name.copy() + + if hasattr(request.cls, 'kwargs_create_item'): + del request.cls.kwargs_create_item + + + +class APIPermissionsTestCases( + APIPermissionsInheritedCases +): + """API Permission Test Cases + + This test suite is dynamically created for `CenturionModel` sub-classes. + Each `CenturionModel` must ensure their model fixture exists in + `tests/fixtures/model_` with fixtures `model_` and + `kwargs_` defined. + """ + pass + + +for centurion_model in get_models( + excludes = [ + 'centurionaudit', + 'history', + 'centurionmodelnote', + 'notes' + ] +): + + if( + not issubclass(centurion_model, CenturionModel) + or centurion_model == CenturionModel + ): + continue + + model_name = centurion_model._meta.model_name + cls_name: str = f"{centurion_model._meta.object_name}APIPermissionsPyTest" + + dynamic_class = type( + cls_name, + (APIPermissionsTestCases,), + { + 'model': make_fixture_with_args( + arg_names = ['model_' + str(centurion_model._meta.model_name) ], + func = model, + decorator_factory = pytest.fixture, + decorator_args = {'scope': 'class'} + + ), + 'model_kwargs': make_fixture_with_args( + arg_names = ['request', f'kwargs_{model_name}' ], + func = model_kwargs, + decorator_factory = pytest.fixture, + decorator_args = {'scope': 'class', 'autouse': True} + ) + } + ) + + model_mark = f'model_{model_name}' + dynamic_class = pytest.mark.__getattr__(model_mark)(dynamic_class) + + globals()[cls_name] = dynamic_class diff --git a/app/devops/tests/functional/git_group/test_functional_git_group_permission.py b/app/devops/tests/functional/git_group/test_functional_git_group_permission.py deleted file mode 100644 index 51d6408c..00000000 --- a/app/devops/tests/functional/git_group/test_functional_git_group_permission.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -from api.tests.functional.test_functional_permissions_api import ( - APIPermissionsInheritedCases, -) - - -@pytest.mark.model_gitgroup -class GitGroupPermissionsAPITestCases( - APIPermissionsInheritedCases, -): - - pass - - - -class GitGroupPermissionsAPIInheritedCases( - GitGroupPermissionsAPITestCases, -): - pass - - -class GitGroupPermissionsAPIPyTest( - GitGroupPermissionsAPITestCases, -): - - pass diff --git a/app/devops/tests/unit/feature_flag/test_unit_feature_flag_model.py b/app/devops/tests/unit/feature_flag/test_unit_feature_flag_model.py index f865ffd7..25725597 100644 --- a/app/devops/tests/unit/feature_flag/test_unit_feature_flag_model.py +++ b/app/devops/tests/unit/feature_flag/test_unit_feature_flag_model.py @@ -8,45 +8,12 @@ from core.tests.unit.centurion_abstract.test_unit_centurion_abstract_model impor -@pytest.mark.model_feature_flag +@pytest.mark.model_featureflag class FeatureFlagModelTestCases( CenturionAbstractModelInheritedCases ): - @pytest.fixture( scope = 'class', autouse = True ) - def software_setup(self, request, django_db_blocker, organization_one): - - from itam.models.software import Software - - with django_db_blocker.unblock(): - - software = Software.objects.create( - organization = organization_one, - name = 'software test' - ) - - request.cls.kwargs_create_item.update({ - 'software': software - }) - - yield - - with django_db_blocker.unblock(): - - software.delete() - - - - kwargs_create_item = { - 'software': None, - 'name': 'a name', - 'description': ' a description', - 'enabled': True, - } - - - @property def parameterized_class_attributes(self): diff --git a/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py b/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py index fa7cfd7d..afe522a0 100644 --- a/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py +++ b/app/itam/tests/unit/software/test_software_item_ticket_api_v2.py @@ -60,7 +60,7 @@ class SoftwareItemTicketAPI( self.url_view_kwargs = { 'item_class': self.item_class, - 'item_id': self.item.id, + 'item_id': self.linked_item.id, 'pk': self.item.id, } diff --git a/app/tests/fixtures/__init__.py b/app/tests/fixtures/__init__.py index a0b47741..855ab711 100644 --- a/app/tests/fixtures/__init__.py +++ b/app/tests/fixtures/__init__.py @@ -62,6 +62,15 @@ from .model_permission import ( model_permission, ) +from .model_software import ( + kwargs_software, + model_software, +) + +from .model_softwareenablefeatureflag import ( + model_softwareenablefeatureflag, +) + from .model_team import ( model_team, ) diff --git a/app/tests/fixtures/kwargs_api_create.py b/app/tests/fixtures/kwargs_api_create.py index 1b462210..f0505801 100644 --- a/app/tests/fixtures/kwargs_api_create.py +++ b/app/tests/fixtures/kwargs_api_create.py @@ -1,5 +1,7 @@ import pytest +from django.db import models + @pytest.fixture(scope = 'class') def kwargs_api_create(django_db_blocker, model_kwargs): diff --git a/app/tests/fixtures/model_centurionmodel.py b/app/tests/fixtures/model_centurionmodel.py index c8ba02ed..e441060b 100644 --- a/app/tests/fixtures/model_centurionmodel.py +++ b/app/tests/fixtures/model_centurionmodel.py @@ -16,7 +16,7 @@ def kwargs_centurionmodel(kwargs_tenancyabstract): kwargs = { **kwargs_tenancyabstract, 'model_notes': 'model notes txt', - 'created': '2025-05-23T00:00', + 'created': '2025-05-23T00:00Z', } yield kwargs.copy() diff --git a/app/tests/fixtures/model_featureflag.py b/app/tests/fixtures/model_featureflag.py index cfa6d608..75b4af6e 100644 --- a/app/tests/fixtures/model_featureflag.py +++ b/app/tests/fixtures/model_featureflag.py @@ -11,17 +11,37 @@ def model_featureflag(request): @pytest.fixture( scope = 'class') -def kwargs_featureflag(kwargs_centurionmodel): +def kwargs_featureflag(django_db_blocker, kwargs_centurionmodel, model_software, kwargs_software, model_softwareenablefeatureflag): - # kwargs = kwargs_centurionmodel.copy() - # del kwargs['model_notes'] - kwargs = { - **kwargs_centurionmodel.copy(), - 'software': None, - 'name': 'a name', - 'description': ' a description', - 'enabled': True, - } + with django_db_blocker.unblock(): - yield kwargs.copy() + kwargs = kwargs_software + + kwargs.update({'name': 'ff_enable_software'}) + + software = model_software.objects.create( + **kwargs + ) + + enable_feature_flag = model_softwareenablefeatureflag.objects.create( + organization = kwargs_centurionmodel['organization'], + software = software, + enabled = True + ) + + kwargs = { + **kwargs_centurionmodel.copy(), + 'software': software, + 'name': 'a name', + 'description': ' a description', + 'enabled': True, + } + + yield kwargs.copy() + + enable_feature_flag.delete() + try: + software.delete() + except: + pass diff --git a/app/tests/fixtures/model_instance.py b/app/tests/fixtures/model_instance.py index b0948a3e..c0a6b326 100644 --- a/app/tests/fixtures/model_instance.py +++ b/app/tests/fixtures/model_instance.py @@ -79,7 +79,10 @@ def model_instance(django_db_blocker, model_user, model, model_kwargs): else: - model_obj.delete() + try: + model_obj.delete() + except: + pass if 'mockmodel' in apps.all_models['core']: diff --git a/app/tests/fixtures/model_software.py b/app/tests/fixtures/model_software.py new file mode 100644 index 00000000..2a726ff1 --- /dev/null +++ b/app/tests/fixtures/model_software.py @@ -0,0 +1,24 @@ +import datetime +import pytest + +from itam.models.software import Software + + + +@pytest.fixture( scope = 'class') +def model_software(request): + + yield Software + + +@pytest.fixture( scope = 'class') +def kwargs_software(kwargs_centurionmodel): + + random_str = str(datetime.datetime.now(tz=datetime.timezone.utc)) + + kwargs = { + **kwargs_centurionmodel.copy(), + 'name': 'software_' + random_str, + } + + yield kwargs.copy() diff --git a/app/tests/fixtures/model_softwareenablefeatureflag.py b/app/tests/fixtures/model_softwareenablefeatureflag.py new file mode 100644 index 00000000..11c47ff2 --- /dev/null +++ b/app/tests/fixtures/model_softwareenablefeatureflag.py @@ -0,0 +1,10 @@ +import pytest + +from devops.models.software_enable_feature_flag import SoftwareEnableFeatureFlag + + + +@pytest.fixture( scope = 'class') +def model_softwareenablefeatureflag(request): + + yield SoftwareEnableFeatureFlag diff --git a/docs/projects/centurion_erp/development/models.md b/docs/projects/centurion_erp/development/models.md index 01c563af..e6b2067c 100644 --- a/docs/projects/centurion_erp/development/models.md +++ b/docs/projects/centurion_erp/development/models.md @@ -277,18 +277,25 @@ The following Unit test suites exists for models: - Functional Tests - - model `core.tests.functional.centurion_abstract.test_functional_centurion_abstract_model.CenturionAbstractModelInheritedCases` - API Fields Render `api.tests.functional.test_functional_api_fields.APIFieldsInheritedCases` - - API Permissions `api.tests.functional.test_functional_api_permissions.InheriredCases` - - Generally Test Cases from class `APIPermissionsInheritedCases` will be used as it covers the standard Django Permissions, `add`, `change`, `delete` and `view`. - !!! info If you add a feature you will have to [write the test cases](./testing.md) for that feature if they are not covered by existing test cases. +Each model has the following Test Suites auto-magic created: + +- API Permissions checks `api.tests.functional.test_functional_meta_permissions_api` + + _Checks the CRUD permissions against the models API endpoints_ + +- Audit History Model checks, `core.tests.unit.centurion_audit_meta.test_unit_meta_audit_history_model` + + _Confirms the model has a [`AuditHistory`](./api/models/audit_history.md) model and other checks as required for an `AuditHistory` model._ + +These auto-magic tests require no input and will be created on a model inheriting from [`CenturionModel`](./api/models/centurion.md) and run every time the tests are run. + ## Knowledge Base Article linking diff --git a/pyproject.toml b/pyproject.toml index 6b4ec291..74393478 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1071,6 +1071,7 @@ markers = [ "centurion_models: Selects Centurion models", "functional: Selects all Functional tests.", "meta_models: Selects Meta models", + "model_featureflag: Feature Flag Model", "model_gitgroup: Selects tests for model `git group`", "models: Selects all models tests.", "note_models: Selects all centurion model note models",