diff --git a/dev/scripts/create-mysql-container.sh b/dev/scripts/create-mysql-container.sh old mode 100644 new mode 100755 diff --git a/dev/scripts/create-postgres-container.sh b/dev/scripts/create-postgres-container.sh new file mode 100755 index 00000000..b43b262d --- /dev/null +++ b/dev/scripts/create-postgres-container.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + + +docker run -d --name dev-surface-psql \ + --health-cmd='pg_isready -U $POSTGRES_USER' --health-interval='5s' \ + -p 35432:5432 \ + -e POSTGRES_PASSWORD=surfdbpassword \ + -e POSTGRES_USER=surface \ + postgres:15.2-alpine diff --git a/surface/surface/tests/__init__.py b/surface/surface/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/surface/surface/tests/test_all_search_and_filter.py b/surface/surface/tests/test_all_search_and_filter.py new file mode 100644 index 00000000..7b22a568 --- /dev/null +++ b/surface/surface/tests/test_all_search_and_filter.py @@ -0,0 +1,75 @@ +from django.contrib.admin.sites import site +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.utils import timezone +from django.db.utils import DataError +from django.db import transaction + + +def _clean_field(fieldname): + # For things like ('time', DateRangeFilter) + if isinstance(fieldname, tuple): + fieldname = fieldname[0] + # Custom field... we will just skip them... + if not isinstance(fieldname, str): + return None + # Remove = from fields + if fieldname[0] == "=": + fieldname = fieldname[1:] + return fieldname + + +def _method_factory(model_class, model_admin): + def method_tester(test_case: TestCase): + # Test the filters and searchfields + for fieldname in list(model_admin.list_filter) + list(model_admin.search_fields): + fieldname = _clean_field(fieldname) + if not fieldname: + continue + try: + try: + # validate that filter can be executed for fieldname, don't care about result + with transaction.atomic(): + test_case.assertIsInstance(model_class.objects.filter(**{fieldname: "1"}).count(), int) + except ValidationError: + # Datetime fields? + test_case.assertIsInstance( + model_class.objects.filter(**{fieldname: timezone.now()}).count(), + int, + ) + except DataError: + # IP fields? + test_case.assertIsInstance( + model_class.objects.filter(**{fieldname: "8.8.8.8"}).count(), + int, + ) + except Exception as e: + test_case.fail(f"{model_admin} search field test failed - {e}") + + return method_tester + + +class AdminMeta(type): + def __init__(cls, *args, **kwargs): + for model_class, model_admin in site._registry.items(): + setattr( + cls, + f'test_{str(model_admin).replace(".", "_")}', + _method_factory(model_class, model_admin), + ) + + +class Test(TestCase, metaclass=AdminMeta): + """ + ## Context + + Metaclass and dynamic test method generation is bad (in principle) for lack of test clarity. + In this case, dynamic was already in place within a single method which prevent proper test result output: + * print would always show up + * if we remove print, when method failed we didn't know which model_admin was bad + + Creating a method per model_admin allows result output to be controlled by the testrunner (verbosity 1 displays only dots, + 2 will display each model_admin as method) and parseable by test result tools + """ + + pass