From 42bea64ff5767af4b505676c1b5bf3945f400701 Mon Sep 17 00:00:00 2001 From: Jon Date: Tue, 22 Apr 2025 22:25:23 +0930 Subject: [PATCH] test(core): API fields Unit Test Suite ref: #731 #730 --- app/api/tests/unit/test_unit_api_fields.py | 193 +++++++++++++++++++++ app/app/tests/common.py | 14 ++ app/conftest.py | 136 +++++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 app/api/tests/unit/test_unit_api_fields.py create mode 100644 app/app/tests/common.py diff --git a/app/api/tests/unit/test_unit_api_fields.py b/app/api/tests/unit/test_unit_api_fields.py new file mode 100644 index 00000000..9855dca3 --- /dev/null +++ b/app/api/tests/unit/test_unit_api_fields.py @@ -0,0 +1,193 @@ +import pytest + +from django.contrib.auth.models import ContentType, Permission, User +from django.shortcuts import reverse +from django.test import Client + +from rest_framework.relations import Hyperlink + +from access.models.team import Team +from access.models.team_user import TeamUsers + +from app.tests.common import DoesNotExist + + + +class APIFieldsTestCases: + + api_fields_common = { + 'id': int, + 'display_name': str, + '_urls': dict, + '_urls._self': str, + '_urls.notes': str + } + + api_fields_model = { + 'model_notes': str, + 'created': str, + 'modified': str + } + + api_fields_tenancy = { + 'organization': dict, + 'organization.id': int, + 'organization.display_name': str, + 'organization.url': Hyperlink, + } + + parametrized_test_data = { + **api_fields_common, + **api_fields_tenancy, + **api_fields_model, + } + + url_view_kwargs = {} + + + + @pytest.fixture( scope = 'class') + def setup_pre(self, + request, + model, + django_db_blocker, + organization_one, + organization_two + ): + + with django_db_blocker.unblock(): + + request.cls.organization = organization_one + + request.cls.different_organization = organization_two + + kwargs_create_item = {} + + for base in reversed(request.cls.__mro__): + + if hasattr(base, 'kwargs_create_item'): + + if base.kwargs_create_item is None: + + continue + + kwargs_create_item.update(**base.kwargs_create_item) + + + if len(kwargs_create_item) > 0: + + request.cls.kwargs_create_item = kwargs_create_item + + + if 'organization' not in request.cls.kwargs_create_item: + + request.cls.kwargs_create_item.update({ + 'organization': request.cls.organization + }) + + view_permissions = Permission.objects.get( + codename = 'view_' + request.cls.model._meta.model_name, + content_type = ContentType.objects.get( + app_label = request.cls.model._meta.app_label, + model = request.cls.model._meta.model_name, + ) + ) + + view_team = Team.objects.create( + team_name = 'cs_api_view_team', + organization = request.cls.organization, + ) + + view_team.permissions.set([view_permissions]) + + + request.cls.view_user = User.objects.create_user(username="cafs_test_user_view", password="password") + + team_user = TeamUsers.objects.create( + team = view_team, + user = request.cls.view_user + ) + + yield + + with django_db_blocker.unblock(): + + team_user.delete() + + view_team.delete() + + request.cls.view_user.delete() + + del request.cls.kwargs_create_item + + + @pytest.fixture( scope = 'class') + def setup_post(self, request, django_db_blocker): + + with django_db_blocker.unblock(): + + request.cls.url_view_kwargs.update({ + 'pk': request.cls.item.id + }) + + client = Client() + url = reverse('v2:' + request.cls.url_ns_name + '-detail', kwargs=request.cls.url_view_kwargs) + + + client.force_login(request.cls.view_user) + response = client.get(url) + + request.cls.api_data = response.data + + yield + + del request.cls.url_view_kwargs['pk'] + + + + + @pytest.fixture( scope = 'class', autouse = True) + def class_setup(self, + setup_pre, + create_model, + setup_post, + ): + + pass + + + def test_api_field_exists(self, recursearray, test_name, test_value, expected): + """Test for existance of API Field""" + + api_data = recursearray(self.api_data, test_value) + + if expected is DoesNotExist: + + assert api_data['key'] not in api_data['obj'] + + else: + + assert api_data['key'] in api_data['obj'] + + + + def test_api_field_type(self, recursearray, test_name, test_value, expected): + """Test for type for API Field""" + + api_data = recursearray(self.api_data, test_value) + + if expected is DoesNotExist: + + assert api_data['key'] not in api_data['obj'] + + else: + + assert type( api_data['value'] ) is expected + + + +class APIFieldsInheritedCases( + APIFieldsTestCases +): + + model = None diff --git a/app/app/tests/common.py b/app/app/tests/common.py new file mode 100644 index 00000000..de65bd68 --- /dev/null +++ b/app/app/tests/common.py @@ -0,0 +1,14 @@ + + + +class DoesNotExist: + """Object does not exist + + Use this class as the expected value for a test cases expected value when + the object does not exist. + """ + + @property + def __name__(self): + + return str('does_not_exist') \ No newline at end of file diff --git a/app/conftest.py b/app/conftest.py index 13b8d469..51027cd1 100644 --- a/app/conftest.py +++ b/app/conftest.py @@ -1,4 +1,5 @@ import pytest +import sys from django.test import ( TestCase @@ -30,6 +31,45 @@ def enable_db_access_for_all_tests(db): # pylint: disable=W0613:unused-argume +def pytest_generate_tests(metafunc): + + # test_no_value = {"test_name", "test_value", "expected"} <= set(metafunc.fixturenames) + + # test_value = {"test_name", "test_value", "return_value", "expected"} <= set(metafunc.fixturenames) + + if {"test_name", "test_value", "expected"} <= set(metafunc.fixturenames): + values = {} + + + cls = getattr(metafunc, "cls", None) + + if cls: + + for base in reversed(cls.__mro__): + + base_values = getattr(base, "parametrized_test_data", []) + + if isinstance(base_values, dict): + + values.update(base_values) + + + if values: + + metafunc.parametrize( + argnames = ( + "test_name", "test_value", "expected" + ), + argvalues = [ + (field, field, expected) for field, expected in values.items() + ], + ids = [ + str( field.replace('.', '_') + '_' + getattr(expected, '__name__', 'None').lower() ) for field, expected in values.items() + ], + ) + + + @pytest.fixture( scope = 'class') def create_model(request, django_db_blocker): @@ -86,3 +126,99 @@ def organization_two(django_db_blocker): with django_db_blocker.unblock(): item.delete() + + + +@pytest.fixture +def recursearray() -> dict[dict, str, any]: + """Recursive dict lookup + + Search through a dot notation of dict paths and return the data assosiated + with that dict. + + Args: + obj (dict): dict to use for the recusive lookup + key (str): the dict keys in dot notation. i.e. dict_1.dict_2 + rtn_dict (bool, optional): return the dictionary and not the value. + Defaults to False. + + Returns: + dict[dict, str, any]: lowest dict found. dict values are obj, key and value. + + """ + + def _recursearray(obj: dict, key:str) -> dict[dict, str, any]: + + item = None + + if key in obj: + + return { + 'obj': obj, + 'key': key, + 'value': obj[key] + } + + keys = [] + + if '.' in key: + + keys = key.split('.') + + + for k, v in obj.items(): + + if k == keys[0] and isinstance(v,dict): + + if keys[1] in v: + + item = _recursearray(v, key.replace(keys[0]+'.', '')) + + return { + 'obj': item['obj'], + 'key': item['key'], + 'value': item['value'] + } + + elif k == keys[0] and isinstance(v,list): + + try: + + list_key = int(keys[1]) + + try: + + item = v[list_key] + + v = item + + + if keys[2] in v: + + item = _recursearray(v, key.replace(keys[0]+'.'+keys[1]+'.', '')) + + return { + 'obj': item['obj'], + 'key': item['key'], + 'value': item['value'] + } + + except IndexError: + + print( f'Index {keys[1]} does not exist. List had a length of {len(v)}', file = sys.stderr ) + + return None + + except ValueError: + + print( f'{keys[1]} does not appear to be a number.', file = sys.stderr ) + + return None + + return { + 'obj': obj, + 'key': key, + 'value': None + } + + return _recursearray