test(api): API Permissions Auto-Creator test suite

ref: #780 #730 #767
This commit is contained in:
2025-06-01 07:16:04 +09:30
parent 3a010e166e
commit 0abb416620
13 changed files with 261 additions and 80 deletions

View File

@ -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_<model_name>` with fixtures `model_<model_name>` and
`kwargs_<model_name>` 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

View File

@ -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

View File

@ -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):

View File

@ -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,
}

View File

@ -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,
)

View File

@ -1,5 +1,7 @@
import pytest
from django.db import models
@pytest.fixture(scope = 'class')
def kwargs_api_create(django_db_blocker, model_kwargs):

View File

@ -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()

View File

@ -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']
with django_db_blocker.unblock():
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': None,
'software': software,
'name': 'a name',
'description': ' a description',
'enabled': True,
}
yield kwargs.copy()
enable_feature_flag.delete()
try:
software.delete()
except:
pass

View File

@ -79,7 +79,10 @@ def model_instance(django_db_blocker, model_user, model, model_kwargs):
else:
try:
model_obj.delete()
except:
pass
if 'mockmodel' in apps.all_models['core']:

24
app/tests/fixtures/model_software.py vendored Normal file
View File

@ -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()

View File

@ -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

View File

@ -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.<permission type>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

View File

@ -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",