From 15b00a6aa8a629f11cb93878dc79d2b020c1fabb Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 12:22:07 +0100 Subject: [PATCH 01/32] [#467] update requirements --- requirements/base.txt | 21 ++++++++++++++++++--- requirements/ci.txt | 26 ++++++++++++++++++++++++-- requirements/dev.txt | 26 ++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index d4cf6b3e..1b1b7555 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,8 @@ # amqp==5.2.0 # via kombu +annotated-types==0.7.0 + # via pydantic ape-pie==0.2.0 # via # commonground-api-common @@ -151,7 +153,7 @@ django-sendfile2==0.7.0 # via django-privates django-sessionprofile==3.0.0 # via open-api-framework -django-setup-configuration==0.1.0 +django-setup-configuration==0.4.0 # via open-api-framework django-simple-certmanager==1.4.1 # via zgw-consumers @@ -260,6 +262,14 @@ psycopg2==2.9.9 # via open-api-framework pycparser==2.20 # via cffi +pydantic==2.9.2 + # via + # django-setup-configuration + # pydantic-settings +pydantic-core==2.23.4 + # via pydantic +pydantic-settings[yaml]==2.6.1 + # via django-setup-configuration pyjwt==2.4.0 # via # commonground-api-common @@ -278,7 +288,9 @@ python-dateutil==2.9.0.post0 python-decouple==3.8 # via open-api-framework python-dotenv==1.0.0 - # via open-api-framework + # via + # open-api-framework + # pydantic-settings pytz==2024.1 # via # drf-yasg @@ -288,6 +300,7 @@ pyyaml==6.0.1 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings qrcode==6.1 # via django-two-factor-auth redis==3.5.3 @@ -321,6 +334,8 @@ tornado==6.4.1 typing-extensions==4.9.0 # via # mozilla-django-oidc-db + # pydantic + # pydantic-core # zgw-consumers tzdata==2024.1 # via celery @@ -349,7 +364,7 @@ webencodings==0.5.1 # via bleach wrapt==1.14.1 # via elastic-apm -zgw-consumers==0.35.1 +zgw-consumers==0.36.1 # via # commonground-api-common # notifications-api-common diff --git a/requirements/ci.txt b/requirements/ci.txt index a9bab76e..f7f994b2 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -8,6 +8,10 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +annotated-types==0.7.0 + # via + # -r requirements/base.txt + # pydantic ape-pie==0.2.0 # via # -r requirements/base.txt @@ -235,10 +239,11 @@ django-sessionprofile==3.0.0 # via # -r requirements/base.txt # open-api-framework -django-setup-configuration==0.1.0 +django-setup-configuration==0.4.0 # via # -r requirements/base.txt # open-api-framework + # zgw-consumers django-simple-certmanager==1.4.1 # via # -r requirements/base.txt @@ -447,6 +452,19 @@ pycparser==2.20 # via # -r requirements/base.txt # cffi +pydantic==2.9.2 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings +pydantic-core==2.23.4 + # via + # -r requirements/base.txt + # pydantic +pydantic-settings[yaml]==2.6.1 + # via + # -r requirements/base.txt + # django-setup-configuration pyflakes==3.2.0 # via flake8 pyjwt==2.4.0 @@ -483,6 +501,7 @@ python-dotenv==1.0.0 # via # -r requirements/base.txt # open-api-framework + # pydantic-settings pytz==2024.1 # via # -r requirements/base.txt @@ -494,6 +513,7 @@ pyyaml==6.0.1 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings # vcrpy qrcode==6.1 # via @@ -552,6 +572,8 @@ typing-extensions==4.9.0 # via # -r requirements/base.txt # mozilla-django-oidc-db + # pydantic + # pydantic-core # zgw-consumers tzdata==2024.1 # via @@ -606,7 +628,7 @@ wrapt==1.14.1 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.35.1 +zgw-consumers[setup-configuration]==0.36.1 # via # -r requirements/base.txt # commonground-api-common diff --git a/requirements/dev.txt b/requirements/dev.txt index 8a115576..6d358b47 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,6 +10,10 @@ amqp==5.2.0 # via # -r requirements/base.txt # kombu +annotated-types==0.7.0 + # via + # -r requirements/base.txt + # pydantic ape-pie==0.2.0 # via # -r requirements/base.txt @@ -250,10 +254,11 @@ django-sessionprofile==3.0.0 # via # -r requirements/base.txt # open-api-framework -django-setup-configuration==0.1.0 +django-setup-configuration==0.4.0 # via # -r requirements/base.txt # open-api-framework + # zgw-consumers django-simple-certmanager==1.4.1 # via # -r requirements/base.txt @@ -470,6 +475,19 @@ pycparser==2.20 # via # -r requirements/base.txt # cffi +pydantic==2.9.2 + # via + # -r requirements/base.txt + # django-setup-configuration + # pydantic-settings +pydantic-core==2.23.4 + # via + # -r requirements/base.txt + # pydantic +pydantic-settings[yaml]==2.6.1 + # via + # -r requirements/base.txt + # django-setup-configuration pyflakes==3.2.0 # via flake8 pygments==2.17.2 @@ -512,6 +530,7 @@ python-dotenv==1.0.0 # via # -r requirements/base.txt # open-api-framework + # pydantic-settings pytz==2024.1 # via # -r requirements/base.txt @@ -524,6 +543,7 @@ pyyaml==6.0.1 # drf-spectacular # drf-yasg # oyaml + # pydantic-settings # vcrpy qrcode==6.1 # via @@ -612,6 +632,8 @@ typing-extensions==4.9.0 # via # -r requirements/base.txt # mozilla-django-oidc-db + # pydantic + # pydantic-core # zgw-consumers tzdata==2024.1 # via @@ -668,7 +690,7 @@ wrapt==1.14.1 # vcrpy yarl==1.9.4 # via vcrpy -zgw-consumers==0.35.1 +zgw-consumers[setup-configuration]==0.36.1 # via # -r requirements/base.txt # commonground-api-common From 93802911f4f6e0a90ee83e03e7ee1338e73492c1 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 12:23:33 +0100 Subject: [PATCH 02/32] [#467] update objecttypes step --- src/objects/conf/base.py | 9 ++- src/objects/config/models.py | 22 ++++++ src/objects/config/objecttypes.py | 107 +++++++++++++++--------------- src/objects/core/models.py | 11 ++- 4 files changed, 89 insertions(+), 60 deletions(-) create mode 100644 src/objects/config/models.py diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index be71aeec..c3914279 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -83,11 +83,10 @@ # # Django setup configuration # -SETUP_CONFIGURATION_STEPS = [ - "objects.config.site.SiteConfigurationStep", - "objects.config.objecttypes.ObjecttypesStep", - "objects.config.demo.DemoUserStep", -] +SETUP_CONFIGURATION_STEPS = ( + "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", + "objects.config.objecttypes.ObjectTypesStep", +) # diff --git a/src/objects/config/models.py b/src/objects/config/models.py new file mode 100644 index 00000000..20714a2a --- /dev/null +++ b/src/objects/config/models.py @@ -0,0 +1,22 @@ +from django_setup_configuration.fields import DjangoModelRef +from django_setup_configuration.models import ConfigurationModel +from zgw_consumers.models import Service +from pydantic import Field + +from objects.core.models import ObjectType + + +class ObjectTypeConfigurationModel(ConfigurationModel): + service_identifier: str = DjangoModelRef(Service, "slug") + name: str = DjangoModelRef(ObjectType, "_name") + + class Meta: + django_model_refs = { + ObjectType: ( + "uuid", + ) + } + + +class ObjectTypesConfigurationModel(ConfigurationModel): + items: list[ObjectTypeConfigurationModel] = Field() diff --git a/src/objects/config/objecttypes.py b/src/objects/config/objecttypes.py index 919fdff7..243c1ce0 100644 --- a/src/objects/config/objecttypes.py +++ b/src/objects/config/objecttypes.py @@ -1,59 +1,60 @@ -from django.conf import settings - -import requests +from django.core.exceptions import ValidationError +from django.db import IntegrityError from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import SelfTestFailed -from zgw_consumers.client import build_client -from zgw_consumers.constants import APITypes, AuthTypes +from django_setup_configuration.exceptions import ConfigurationRunFailed +from zgw_consumers.contrib.setup_configuration.models import SingleServiceConfigurationModel from zgw_consumers.models import Service +from objects.config.models import ObjectTypesConfigurationModel +from objects.core.models import ObjectType -class ObjecttypesStep(BaseConfigurationStep): - """ - Configure credentials for Objects API to request Objecttypes API - - Normal mode doesn't change the token after its initial creation. - If the token is changed, run this command with 'overwrite' flag - """ +# TODO: remove previously used django settings? +# TODO: update documenation +class ObjectTypesConfigurationStep(BaseConfigurationStep): + config_model = ObjectTypesConfigurationModel verbose_name = "Objecttypes Configuration" - required_settings = [ - "OBJECTTYPES_API_ROOT", - "OBJECTS_OBJECTTYPES_TOKEN", - ] - enable_setting = "OBJECTS_OBJECTTYPES_CONFIG_ENABLE" - - def is_configured(self) -> bool: - return Service.objects.filter(api_root=settings.OBJECTTYPES_API_ROOT).exists() - - def configure(self) -> None: - Service.objects.update_or_create( - api_root=settings.OBJECTTYPES_API_ROOT, - defaults={ - "label": "Objecttypes API", - "api_type": APITypes.orc, - "oas": settings.OBJECTTYPES_API_OAS, - "auth_type": AuthTypes.api_key, - "header_key": "Authorization", - "header_value": f"Token {settings.OBJECTS_OBJECTTYPES_TOKEN}", - }, - ) - - def test_configuration(self) -> None: - """ - This check depends on the configuration in Objecttypes - """ - client = build_client( - Service.objects.get(api_root=settings.OBJECTTYPES_API_ROOT) - ) - try: - response = client.get("objecttypes") - except requests.RequestException as exc: - raise SelfTestFailed( - "Could not Could not retrieve list of objecttypes from Objecttypes API." - ) from exc - - try: - response.json() - except requests.exceptions.JSONDecodeError: - raise SelfTestFailed("Object type version didn't have any data") + + namespace = "objecttypes" + enable_setting = "objecttypes_config_enable" + + def execute(self, model: ObjectTypesConfigurationModel) -> None: + for item in model.items: + try: + service = Service.objects.get(slug=item.service_identifier) + except Service.DoesNotExist: + raise ConfigurationRunFailed( + f"No service found with identifier {item.service_identifier}" + ) + + objecttype_kwargs = dict( + service=service, + uuid=item.uuid, + _name=item.name, + ) + + objecttype_instance = ObjectType(**objecttype_kwargs) + + try: + objecttype_instance.full_clean( + exclude=("id", "service"), validate_unique=False + ) + except ValidationError as exception: + exception_message = ( + f"Validation error(s) occured for objecttype {item.uuid}." + ) + raise ConfigurationRunFailed(exception_message) from exception + + try: + ObjectType.objects.update_or_create( + uuid=item.uuid, + defaults={ + key: value for key, value in objecttype_kwargs.items() + if key != "uuid" + } + ) + except IntegrityError as exception: + exception_message = ( + f"Failed configuring ObjectType {item.uuid}." + ) + raise ConfigurationRunFailed(exception_message) from exception diff --git a/src/objects/core/models.py b/src/objects/core/models.py index ccf4fd36..3df3af1f 100644 --- a/src/objects/core/models.py +++ b/src/objects/core/models.py @@ -1,4 +1,5 @@ import datetime +from typing import Iterable, Optional import uuid from django.contrib.gis.db.models import GeometryField @@ -41,8 +42,14 @@ def url(self): # zds_client.get_operation_url() can be used here but it increases HTTP overhead return f"{self.service.api_root}objecttypes/{self.uuid}" - def clean(self): + def clean_fields(self, exclude: Optional[Iterable[str]] = None) -> None: + super().clean_fields(exclude=exclude) + + if exclude and "service" in exclude: + return + client = build_client(self.service) + try: response = client.get(url=self.url) except (requests.RequestException, ConnectionError, ValueError) as exc: @@ -51,7 +58,7 @@ def clean(self): try: object_type_data = response.json() except requests.exceptions.JSONDecodeError: - ValidationError("Object type version didn't have any data") + raise ValidationError("Object type version didn't have any data") if not self._name: self._name = object_type_data["name"] From 4c605eca34460243c112ac14a7205911ab809d84 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 12:23:43 +0100 Subject: [PATCH 03/32] [#467] add tests --- .../files/objecttypes_empty_database.yaml | 10 + .../objecttypes_existing_objecttype.yaml | 10 + .../tests/files/objecttypes_idempotent.yaml | 10 + .../tests/files/objecttypes_invalid_uuid.yaml | 10 + .../files/objecttypes_unknown_service.yaml | 10 + .../core/tests/test_objecttype_config.py | 191 ++++++++++++++++++ 6 files changed, 241 insertions(+) create mode 100644 src/objects/core/tests/files/objecttypes_empty_database.yaml create mode 100644 src/objects/core/tests/files/objecttypes_existing_objecttype.yaml create mode 100644 src/objects/core/tests/files/objecttypes_idempotent.yaml create mode 100644 src/objects/core/tests/files/objecttypes_invalid_uuid.yaml create mode 100644 src/objects/core/tests/files/objecttypes_unknown_service.yaml create mode 100644 src/objects/core/tests/test_objecttype_config.py diff --git a/src/objects/core/tests/files/objecttypes_empty_database.yaml b/src/objects/core/tests/files/objecttypes_empty_database.yaml new file mode 100644 index 00000000..b969949e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_empty_database.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml new file mode 100644 index 00000000..f93e005f --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: 7229549b-7b41-47d1-8106-414b2a69751b + name: Object Type 3 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_idempotent.yaml b/src/objects/core/tests/files/objecttypes_idempotent.yaml new file mode 100644 index 00000000..b969949e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_idempotent.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml new file mode 100644 index 00000000..2a360c8e --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 + + - uuid: foobar + name: Object Type 2 + service_identifier: service-1 diff --git a/src/objects/core/tests/files/objecttypes_unknown_service.yaml b/src/objects/core/tests/files/objecttypes_unknown_service.yaml new file mode 100644 index 00000000..8348427c --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_unknown_service.yaml @@ -0,0 +1,10 @@ +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: unknown + + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-1 diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py new file mode 100644 index 00000000..cb919ada --- /dev/null +++ b/src/objects/core/tests/test_objecttype_config.py @@ -0,0 +1,191 @@ +from pathlib import Path + +from django.db.models import QuerySet +from django.test import TestCase, override_settings + +from django_setup_configuration.exceptions import ConfigurationRunFailed +from django_setup_configuration.test_utils import execute_single_step +from zgw_consumers.models import Service +from zgw_consumers.test.factories import ServiceFactory + +from objects.core.models import ObjectType +from objects.core.tests.factories import ObjectTypeFactory +from objects.config.objecttypes import ObjectTypesConfigurationStep + +TEST_FILES = (Path(__file__).parent / "files").resolve() + + +@override_settings(ZGW_CONSUMERS_IGNORE_OAS_FIELDS=True) +class ObjectTypesConfigurationStepTests(TestCase): + def test_empty_database(self): + service_1 = ServiceFactory(slug="service-1") + service_2 = ServiceFactory(slug="service-2") + + test_file_path = str(TEST_FILES / "objecttypes_empty_database.yaml") + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") + + self.assertEqual(objecttypes.count(), 2) + + objecttype_1: ObjectType = objecttypes.first() + + self.assertEqual( + str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2: ObjectType = objecttypes.last() + + self.assertEqual( + str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" + ) + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) + + def test_existing_objecttype(self): + test_file_path = str(TEST_FILES / "objecttypes_existing_objecttype.yaml") + + service_1: Service = ServiceFactory(slug="service-1") + service_2: Service = ServiceFactory(slug="service-2") + + objecttype_1: ObjectType = ObjectTypeFactory( + service=service_1, + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001" + ) + objecttype_2: ObjectType = ObjectTypeFactory( + service=service_2, + uuid="b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2", + _name="Object Type 002" + ) + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + self.assertEqual(ObjectType.objects.count(), 3) + + objecttype_1.refresh_from_db() + + self.assertEqual( + str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2.refresh_from_db() + + self.assertEqual( + str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" + ) + self.assertEqual(objecttype_2._name, "Object Type 002") + self.assertEqual(objecttype_2.service, service_2) + + objecttype_3: ObjectType = ObjectType.objects.get( + uuid="7229549b-7b41-47d1-8106-414b2a69751b" + ) + + self.assertEqual( + str(objecttype_3.uuid), "7229549b-7b41-47d1-8106-414b2a69751b" + ) + self.assertEqual(objecttype_3._name, "Object Type 3") + self.assertEqual(objecttype_3.service, service_2) + + def test_unknown_service(self): + service = ServiceFactory(slug="service-1") + + objecttype: ObjectType = ObjectTypeFactory( + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001", + service=service + ) + + test_file_path = str(TEST_FILES / "objecttypes_unknown_service.yaml") + + with self.assertRaises(ConfigurationRunFailed): + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + self.assertEqual(ObjectType.objects.count(), 1) + + objecttype.refresh_from_db() + + self.assertEqual( + str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype._name, "Object Type 001") + self.assertEqual(objecttype.service, service) + + def test_invalid_uuid(self): + test_file_path = str(TEST_FILES / "objecttypes_invalid_uuid.yaml") + + service: Service = ServiceFactory(slug="service-1") + + objecttype: ObjectType = ObjectTypeFactory( + service=service, + uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", + _name="Object Type 001" + ) + + with self.assertRaises(ConfigurationRunFailed): + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + self.assertEqual(ObjectType.objects.count(), 1) + + objecttype.refresh_from_db() + + self.assertEqual( + str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype._name, "Object Type 1") + self.assertEqual(objecttype.service, service) + + def test_idempotent_step(self): + service_1 = ServiceFactory(slug="service-1") + service_2 = ServiceFactory(slug="service-2") + + test_file_path = str(TEST_FILES / "objecttypes_idempotent.yaml") + + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") + + self.assertEqual(objecttypes.count(), 2) + + objecttype_1: ObjectType = objecttypes.first() + + self.assertEqual( + str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + objecttype_2: ObjectType = objecttypes.last() + + self.assertEqual( + str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" + ) + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) + + # Rerun + execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + + objecttype_1.refresh_from_db() + objecttype_2.refresh_from_db() + + self.assertEqual(ObjectType.objects.count(), 2) + + # objecttype 1 + self.assertEqual( + str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" + ) + self.assertEqual(objecttype_1._name, "Object Type 1") + self.assertEqual(objecttype_1.service, service_1) + + # objecttype 2 + self.assertEqual( + str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" + ) + self.assertEqual(objecttype_2._name, "Object Type 2") + self.assertEqual(objecttype_2.service, service_2) From 38da55db284a3907645d1c5c08d19338ce9ec2af Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 12:36:59 +0100 Subject: [PATCH 04/32] [#467] update documentation --- docs/installation/config_cli.rst | 95 +++++++++---------------------- src/objects/config/objecttypes.py | 1 - 2 files changed, 28 insertions(+), 68 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 79e499b9..0f733163 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -8,10 +8,6 @@ Configuration (CLI) After deploying Objecttypes API and Objects API, they need to be configured to be fully functional. The command line tool `setup_configuration`_ assist with this configuration: -* It uses environment variables for all configuration choices, therefore you can integrate this with your - infrastructure tooling such as init containers and/or Kubernetes Jobs. -* The command can self-test the configuration to detect problems early on - You can get the full command documentation with: .. code-block:: bash @@ -28,9 +24,9 @@ Preparation =========== The command executes the list of pluggable configuration steps, and each step -required specific environment variables, that should be prepared. -Here is the description of all available configuration steps and the environment variables, -use by each step for both APIs. +requires specific configuration information, that should be prepared. +Here is the description of all available configuration steps and the configuration +format, use by each step. Objects API @@ -39,75 +35,55 @@ Objects API Sites configuration ------------------- -Configure the domain where Objects API is hosted +.. + _TODO: -* ``SITES_CONFIG_ENABLE``: enable Site configuration. Defaults to ``False``. -* ``OBJECTS_DOMAIN``: a ``[host]:[port]`` or ``[host]`` value. Required. -* ``OBJECTS_ORGANIZATION``: name of Objects API organization. Required. Objecttypes configuration ------------------------- -Objects API uses Objecttypes API to validate data against JSON schemas, therefore -it should be able to request Objecttypes API. +Create a (single) YAML configuration file with your settings: -* ``OBJECTS_OBJECTTYPES_CONFIG_ENABLE``: enable Objecttypes configuration. Defaults - to ``False``. -* ``OBJECTTYPES_API_ROOT``: full URL to the Objecttypes API root, for example - ``https://objecttypes.gemeente.local/api/v1/``. Required. -* ``OBJECTTYPES_API_OAS``: full URL to the Objecttypes OpenAPI specification. -* ``OBJECTS_OBJECTTYPES_TOKEN``: authorization token. Required. -* ``OBJECTS_OBJECTTYPES_PERSON``: Objects API contact person. Required. -* ``OBJECTS_OBJECTTYPES_EMAIL``: Objects API contact email. Required. +.. code-block:: yaml + objecttypes_config_enable: true + objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: service-1 -Demo user configuration ------------------------ + - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 + name: Object Type 2 + service_identifier: service-2 -Demo user can be created to check if Objects API work. It has superuser permissions, -so its creation is not recommended on production environment. +.. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. -* ``DEMO_CONFIG_ENABLE``: enable demo user configuration. Defaults to ``False``. -* ``DEMO_PERSON``: demo user contact person. Required. -* ``DEMO_EMAIL``: demo user email. Required. -* ``DEMO_TOKEN``: demo token. Required. +Demo user configuration +----------------------- +.. + _TODO: -Objecttypes API -=============== - -ObjectTypes API has similar configuration steps as the Objects API. Sites configuration ------------------- -Configure the domain where Objects API is hosted +.. + _TODO: -* ``SITES_CONFIG_ENABLE``: enable Site configuration. Defaults to ``False``. -* ``OBJECTTYPES_DOMAIN``: a ``[host]:[port]`` or ``[host]`` value. Required. -* ``OBJECTTYPES_ORGANIZATION``: name of Objecttypes API organization. Required. Objects configuration --------------------- -Objects API uses Objecttypes API to validate data against JSON schemas, therefore -it should be able to request Objecttypes API. +.. + _TODO: -* ``OBJECTS_OBJECTTYPES_CONFIG_ENABLE``: enable Objecttypes configuration. Defaults - to ``False``. -* ``OBJECTTYPES_API_ROOT``: full URL to the Objecttypes API root, for example - ``https://objecttypes.gemeente.local/api/v1/``. Required. -* ``OBJECTTYPES_API_OAS``: full URL to the Objecttypes OpenAPI specification. -* ``OBJECTS_OBJECTTYPES_TOKEN``: authorization token. Required. Demo user configuration ----------------------- -The similar configuration as in Objects API. - -* ``DEMO_CONFIG_ENABLE``: enable demo user configuration. Defaults to ``False``. -* ``DEMO_PERSON``: demo user contact person. Required. -* ``DEMO_EMAIL``: demo user email. Required. -* ``DEMO_TOKEN``: demo token. Required. +.. + _TODO: Execution @@ -119,19 +95,4 @@ tested. .. code-block:: bash - src/manage.py setup_configuration - - -You can skip the self-tests by using the ``--no-selftest`` flag. - -.. code-block:: bash - - src/manage.py setup_configuration --no-self-test - - -``setup_configuration`` command checks if the configuration already exists before changing it. -If you want to change some of the values of the existing configuration you can use ``--overwrite`` flag. - -.. code-block:: bash - - src/manage.py setup_configuration --overwrite + src/manage.py setup_configuration --yaml-file /path/to/config.yaml diff --git a/src/objects/config/objecttypes.py b/src/objects/config/objecttypes.py index 243c1ce0..41bb050e 100644 --- a/src/objects/config/objecttypes.py +++ b/src/objects/config/objecttypes.py @@ -10,7 +10,6 @@ # TODO: remove previously used django settings? -# TODO: update documenation class ObjectTypesConfigurationStep(BaseConfigurationStep): config_model = ObjectTypesConfigurationModel verbose_name = "Objecttypes Configuration" From 19a8008edc65b52679fa5839bd03d66f9a544264 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 14:52:34 +0100 Subject: [PATCH 05/32] [#467] add missing optional requirement for zgw-consumers --- requirements/base.in | 1 + requirements/base.txt | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/requirements/base.in b/requirements/base.in index 1912e5b4..8621c029 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -7,3 +7,4 @@ furl # Common ground libraries notifications-api-common +zgw-consumers[setup-configuration] diff --git a/requirements/base.txt b/requirements/base.txt index 1b1b7555..716ada15 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -154,7 +154,9 @@ django-sendfile2==0.7.0 django-sessionprofile==3.0.0 # via open-api-framework django-setup-configuration==0.4.0 - # via open-api-framework + # via + # open-api-framework + # zgw-consumers django-simple-certmanager==1.4.1 # via zgw-consumers django-solo==2.2.0 @@ -364,8 +366,9 @@ webencodings==0.5.1 # via bleach wrapt==1.14.1 # via elastic-apm -zgw-consumers==0.36.1 +zgw-consumers[setup-configuration]==0.36.1 # via + # -r requirements/base.in # commonground-api-common # notifications-api-common # open-api-framework From 89f8d6031f68742730ea0d9b853f88dc468fc9e6 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 15:10:15 +0100 Subject: [PATCH 06/32] [#467] remove previous test suite --- src/objects/tests/commands/__init__.py | 0 .../commands/test_setup_configuration.py | 123 ------------------ 2 files changed, 123 deletions(-) delete mode 100644 src/objects/tests/commands/__init__.py delete mode 100644 src/objects/tests/commands/test_setup_configuration.py diff --git a/src/objects/tests/commands/__init__.py b/src/objects/tests/commands/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/objects/tests/commands/test_setup_configuration.py b/src/objects/tests/commands/test_setup_configuration.py deleted file mode 100644 index 61b0d656..00000000 --- a/src/objects/tests/commands/test_setup_configuration.py +++ /dev/null @@ -1,123 +0,0 @@ -from io import StringIO - -from django.contrib.sites.models import Site -from django.core.management import CommandError, call_command -from django.test import TestCase, override_settings -from django.urls import reverse - -import requests_mock -from rest_framework import status -from zgw_consumers.client import build_client -from zgw_consumers.models import Service - -from objects.config.demo import DemoUserStep -from objects.config.objecttypes import ObjecttypesStep -from objects.config.site import SiteConfigurationStep - -from ..utils import mock_service_oas_get - - -@override_settings( - SITES_CONFIG_ENABLE=True, - OBJECTS_DOMAIN="objects.example.com", - OBJECTS_ORGANIZATION="ACME", - OBJECTS_OBJECTTYPES_CONFIG_ENABLE=True, - OBJECTTYPES_API_ROOT="https://objecttypes.example.com/api/v2/", - OBJECTTYPES_API_OAS="https://objecttypes.example.com/api/v2/schema/openapi.yaml", - OBJECTS_OBJECTTYPES_TOKEN="some-random-string", - DEMO_CONFIG_ENABLE=True, - DEMO_TOKEN="demo-random-string", - DEMO_PERSON="Demo", - DEMO_EMAIL="demo@demo.local", -) -class SetupConfigurationTests(TestCase): - def setUp(self): - super().setUp() - - self.addCleanup(Site.objects.clear_cache) - - @requests_mock.Mocker() - def test_setup_configuration(self, m): - stdout = StringIO() - # mocks - m.get("http://objects.example.com/", status_code=200) - m.get("http://objects.example.com/api/v2/objects", json=[]) - mock_service_oas_get( - m, "https://objecttypes.example.com/api/v2/", "objecttypes" - ) - m.get("https://objecttypes.example.com/api/v2/objecttypes", json={}) - - call_command("setup_configuration", stdout=stdout) - - with self.subTest("Command output"): - command_output = stdout.getvalue().splitlines() - expected_output = [ - f"Configuration will be set up with following steps: [{SiteConfigurationStep()}, " - f"{ObjecttypesStep()}, {DemoUserStep()}]", - f"Configuring {SiteConfigurationStep()}...", - f"{SiteConfigurationStep()} is successfully configured", - f"Configuring {ObjecttypesStep()}...", - f"{ObjecttypesStep()} is successfully configured", - f"Configuring {DemoUserStep()}...", - f"{DemoUserStep()} is successfully configured", - "Instance configuration completed.", - ] - - self.assertEqual(command_output, expected_output) - - with self.subTest("Site configured correctly"): - site = Site.objects.get_current() - self.assertEqual(site.domain, "objects.example.com") - self.assertEqual(site.name, "Objects ACME") - - with self.subTest("Objects can query Objecttypes API"): - client = build_client( - Service.objects.get(api_root="https://objecttypes.example.com/api/v2/") - ) - - self.assertIsNotNone(client) - - client.get("objecttypes") - - list_call = m.last_request - self.assertEqual( - list_call.url, "https://objecttypes.example.com/api/v2/objecttypes" - ) - self.assertIn("Authorization", list_call.headers) - self.assertEqual( - list_call.headers["Authorization"], "Token some-random-string" - ) - - with self.subTest("Demo user configured correctly"): - response = self.client.get( - reverse("v2:object-list"), - HTTP_AUTHORIZATION="Token demo-random-string", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @requests_mock.Mocker() - def test_setup_configuration_selftest_fails(self, m): - m.get("http://objects.example.com/", status_code=500) - m.get("http://objects.example.com/api/v2/objects", status_code=200) - mock_service_oas_get( - m, "https://objecttypes.example.com/api/v2/", "objecttypes" - ) - m.get("https://objecttypes.example.com/api/v2/objecttypes", json={}) - - with self.assertRaisesMessage( - CommandError, - "Configuration test failed with errors: " - "Site Configuration: Could not access home page at 'http://objects.example.com/'", - ): - call_command("setup_configuration") - - @requests_mock.Mocker() - def test_setup_configuration_without_selftest(self, m): - stdout = StringIO() - - call_command("setup_configuration", no_selftest=True, stdout=stdout) - command_output = stdout.getvalue() - - self.assertEqual(len(m.request_history), 0) - self.assertTrue("Selftest is skipped" in command_output) From 842389231ed164cf3aac522b477a8b9ebda9534b Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 15:12:34 +0100 Subject: [PATCH 07/32] [#467] update docker compose setup & remove previous setup configuration settings --- bin/setup_configuration.sh | 14 ++++++--- docker-compose.yml | 46 ++++++++++++---------------- docker/setup_configuration/data.yaml | 16 ++++++++++ src/objects/conf/base.py | 31 +------------------ src/objects/config/objecttypes.py | 2 -- 5 files changed, 46 insertions(+), 63 deletions(-) create mode 100644 docker/setup_configuration/data.yaml diff --git a/bin/setup_configuration.sh b/bin/setup_configuration.sh index 716b22e8..19f53f00 100755 --- a/bin/setup_configuration.sh +++ b/bin/setup_configuration.sh @@ -1,10 +1,14 @@ #!/bin/bash -# setup initial configuration using environment variables +# setup initial configuration using an yaml file # Run this script from the root of the repository -#set -e -${SCRIPTPATH}/wait_for_db.sh +set -e -src/manage.py migrate -src/manage.py setup_configuration --no-selftest +if [[ "${RUN_SETUP_CONFIG,,}" =~ ^(true|1|yes)$ ]]; then + # wait for required services + /wait_for_db.sh + + src/manage.py migrate + src/manage.py setup_configuration --yaml-file setup_configuration/data.yaml +fi diff --git a/docker-compose.yml b/docker-compose.yml index 9ac0ce1d..e1ff1f45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,28 +16,17 @@ services: build: &web_build context: . environment: &web_env - - DJANGO_SETTINGS_MODULE=objects.conf.docker - - SECRET_KEY=${SECRET_KEY:-1(@f(-6s_u(5fd&1sg^uvu2s(c-9sapw)1era8q&)g)h@cwxxg} - - IS_HTTPS=no - - ALLOWED_HOSTS=* - - CACHE_DEFAULT=redis:6379/0 - - CACHE_AXES=redis:6379/0 - - CELERY_BROKER_URL=redis://redis:6379/1 - - CELERY_RESULT_BACKEND=redis://redis:6379/1 - - CELERY_LOGLEVEL=DEBUG - - DISABLE_2FA=${DISABLE_2FA:-yes} - - SUBPATH=${SUBPATH} - # setup_configuration env vars - - SITES_CONFIG_ENABLE=yes - - OBJECTS_DOMAIN=web:8000 - - OBJECTS_ORGANIZATION=Objects - - OBJECTS_OBJECTTYPES_CONFIG_ENABLE=false - # - OBJECTTYPES_API_ROOT=https://objecttypes.example.com/api/v2/ - - OBJECTS_OBJECTTYPES_TOKEN=some-random-string - - DEMO_CONFIG_ENABLE=yes - - DEMO_TOKEN=demo-random-string - - DEMO_PERSON=Demo - - DEMO_EMAIL=demo@demo.local + DJANGO_SETTINGS_MODULE: objects.conf.docker + SECRET_KEY: ${SECRET_KEY:-1(@f(-6s_u(5fd&1sg^uvu2s(c-9sapw)1era8q&)g)h@cwxxg} + IS_HTTPS: no + ALLOWED_HOSTS: '*' + CACHE_DEFAULT: redis:6379/0 + CACHE_AXES: redis:6379/0 + CELERY_BROKER_URL: redis://redis:6379/1 + CELERY_RESULT_BACKEND: redis://redis:6379/1 + CELERY_LOGLEVEL: DEBUG + DISABLE_2FA: ${DISABLE_2FA:-yes} + SUBPATH: ${SUBPATH} healthcheck: test: ["CMD", "python", "-c", "import requests; exit(requests.head('http://localhost:8000/admin/').status_code not in [200, 302])"] interval: 30s @@ -46,7 +35,7 @@ services: # This should allow for enough time for migrations to run before the max # retries have passed. This healthcheck in turn allows other containers # to wait for the database migrations. - start_period: 30s + start_period: 30s ports: - 8000:8000 depends_on: @@ -58,12 +47,17 @@ services: web-init: image: maykinmedia/objects-api:latest build: *web_build - environment: *web_env + environment: + <<: *web_env + # + # Django-setup-configuration + RUN_SETUP_CONFIG: ${RUN_SETUP_CONFIG:-true} command: /setup_configuration.sh depends_on: - db - redis - volumes: *web_volumes + volumes: + - ./docker/setup_configuration:/app/setup_configuration celery: image: maykinmedia/objects-api:latest @@ -75,7 +69,7 @@ services: interval: 30s timeout: 5s retries: 3 - start_period: 10s + start_period: 10s depends_on: web: condition: service_healthy diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml new file mode 100644 index 00000000..92eb6326 --- /dev/null +++ b/docker/setup_configuration/data.yaml @@ -0,0 +1,16 @@ +zgw_consumers_config_enable: true +zgw_consumers: + services: + # minimum required fields + - identifier: objecttypen-test + label: Objecttypen API test + api_root: http://objecttypen.local/api/v1/ + api_type: orc + auth_type: api_key + +objecttypes_config_enable: true +objecttypes: + items: + - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 + name: Object Type 1 + service_identifier: objecttypen-test diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index c3914279..af77677f 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -85,34 +85,5 @@ # SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", - "objects.config.objecttypes.ObjectTypesStep", + "objects.config.objecttypes.ObjectTypesConfigurationStep", ) - - -# -# Objecttypes settings -# - -# setup_configuration command -# sites config -SITES_CONFIG_ENABLE = config("SITES_CONFIG_ENABLE", default=False, add_to_docs=False) -OBJECTS_DOMAIN = config("OBJECTS_DOMAIN", "", add_to_docs=False) -OBJECTS_ORGANIZATION = config("OBJECTS_ORGANIZATION", "", add_to_docs=False) -# objecttypes config -OBJECTS_OBJECTTYPES_CONFIG_ENABLE = config( - "OBJECTS_OBJECTTYPES_CONFIG_ENABLE", default=False, add_to_docs=False -) -OBJECTTYPES_API_ROOT = config("OBJECTTYPES_API_ROOT", "", add_to_docs=False) -if OBJECTTYPES_API_ROOT and not OBJECTTYPES_API_ROOT.endswith("/"): - OBJECTTYPES_API_ROOT = f"{OBJECTTYPES_API_ROOT.strip()}/" -OBJECTTYPES_API_OAS = config( - "OBJECTTYPES_API_OAS", - default=f"{OBJECTTYPES_API_ROOT}schema/openapi.yaml", - add_to_docs=False, -) -OBJECTS_OBJECTTYPES_TOKEN = config("OBJECTS_OBJECTTYPES_TOKEN", "", add_to_docs=False) -# Demo User Configuration -DEMO_CONFIG_ENABLE = config("DEMO_CONFIG_ENABLE", default=False, add_to_docs=False) -DEMO_TOKEN = config("DEMO_TOKEN", "", add_to_docs=False) -DEMO_PERSON = config("DEMO_PERSON", "", add_to_docs=False) -DEMO_EMAIL = config("DEMO_EMAIL", "", add_to_docs=False) diff --git a/src/objects/config/objecttypes.py b/src/objects/config/objecttypes.py index 41bb050e..6dc58c76 100644 --- a/src/objects/config/objecttypes.py +++ b/src/objects/config/objecttypes.py @@ -2,14 +2,12 @@ from django.db import IntegrityError from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed -from zgw_consumers.contrib.setup_configuration.models import SingleServiceConfigurationModel from zgw_consumers.models import Service from objects.config.models import ObjectTypesConfigurationModel from objects.core.models import ObjectType -# TODO: remove previously used django settings? class ObjectTypesConfigurationStep(BaseConfigurationStep): config_model = ObjectTypesConfigurationModel verbose_name = "Objecttypes Configuration" From 6fb8d832b54df996abcf64068ebdc012bfab65f6 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 15:25:28 +0100 Subject: [PATCH 08/32] [#467] update setup configuration project structure --- src/objects/conf/base.py | 4 +- src/objects/config/demo.py | 56 ------------- src/objects/config/site.py | 37 --------- .../core/tests/test_objecttype_config.py | 2 +- .../__init__.py | 0 .../models/objecttypes.py} | 0 .../steps}/objecttypes.py | 2 +- .../tests/config/test_demo_configuration.py | 73 ----------------- .../config/test_objecttypes_configuration.py | 80 ------------------- .../tests/config/test_site_configuration.py | 66 --------------- 10 files changed, 4 insertions(+), 316 deletions(-) delete mode 100644 src/objects/config/demo.py delete mode 100644 src/objects/config/site.py rename src/objects/{config => setup_configuration}/__init__.py (100%) rename src/objects/{config/models.py => setup_configuration/models/objecttypes.py} (100%) rename src/objects/{config => setup_configuration/steps}/objecttypes.py (95%) delete mode 100644 src/objects/tests/config/test_demo_configuration.py delete mode 100644 src/objects/tests/config/test_objecttypes_configuration.py delete mode 100644 src/objects/tests/config/test_site_configuration.py diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index af77677f..87df16bf 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -18,7 +18,7 @@ # Project applications. "objects.accounts", "objects.api", - "objects.config", + "objects.setup_configuration", "objects.core", "objects.token", "objects.utils", @@ -85,5 +85,5 @@ # SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", - "objects.config.objecttypes.ObjectTypesConfigurationStep", + "objects.setup_configuration.steps.objecttypes.ObjectTypesConfigurationStep", ) diff --git a/src/objects/config/demo.py b/src/objects/config/demo.py deleted file mode 100644 index 8155717c..00000000 --- a/src/objects/config/demo.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.conf import settings -from django.urls import reverse - -import requests -from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import SelfTestFailed - -from objects.token.models import TokenAuth -from objects.utils import build_absolute_url - - -class DemoUserStep(BaseConfigurationStep): - """ - Create demo user to request Objects API - - **NOTE** For now demo user has all permissions. - """ - - verbose_name = "Demo User Configuration" - required_settings = [ - "DEMO_TOKEN", - "DEMO_PERSON", - "DEMO_EMAIL", - ] - enable_setting = "DEMO_CONFIG_ENABLE" - - def is_configured(self) -> bool: - return TokenAuth.objects.filter(token=settings.DEMO_TOKEN).exists() - - def configure(self): - TokenAuth.objects.update_or_create( - token=settings.DEMO_TOKEN, - defaults={ - "contact_person": settings.DEMO_PERSON, - "email": settings.DEMO_EMAIL, - "is_superuser": True, - }, - ) - - def test_configuration(self): - endpoint = reverse("v2:object-list") - full_url = build_absolute_url(endpoint, request=None) - - try: - response = requests.get( - full_url, - headers={ - "Authorization": f"Token {settings.DEMO_TOKEN}", - "Accept": "application/json", - }, - ) - response.raise_for_status() - except requests.RequestException as exc: - raise SelfTestFailed( - "Could not list objects for the configured token" - ) from exc diff --git a/src/objects/config/site.py b/src/objects/config/site.py deleted file mode 100644 index af20fb0e..00000000 --- a/src/objects/config/site.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.conf import settings -from django.contrib.sites.models import Site -from django.urls import reverse - -import requests -from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import SelfTestFailed - -from objects.utils import build_absolute_url - - -class SiteConfigurationStep(BaseConfigurationStep): - """ - Configure the application site/domain. - """ - - verbose_name = "Site Configuration" - required_settings = ["OBJECTS_DOMAIN", "OBJECTS_ORGANIZATION"] - enable_setting = "SITES_CONFIG_ENABLE" - - def is_configured(self) -> bool: - site = Site.objects.get_current() - return site.domain == settings.OBJECTS_DOMAIN - - def configure(self): - site = Site.objects.get_current() - site.domain = settings.OBJECTS_DOMAIN - site.name = f"Objects {settings.OBJECTS_ORGANIZATION}".strip() - site.save() - - def test_configuration(self): - full_url = build_absolute_url(reverse("home")) - try: - response = requests.get(full_url) - response.raise_for_status() - except requests.RequestException as exc: - raise SelfTestFailed(f"Could not access home page at '{full_url}'") from exc diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py index cb919ada..9ecb86bb 100644 --- a/src/objects/core/tests/test_objecttype_config.py +++ b/src/objects/core/tests/test_objecttype_config.py @@ -10,7 +10,7 @@ from objects.core.models import ObjectType from objects.core.tests.factories import ObjectTypeFactory -from objects.config.objecttypes import ObjectTypesConfigurationStep +from objects.setup_configuration.steps.objecttypes import ObjectTypesConfigurationStep TEST_FILES = (Path(__file__).parent / "files").resolve() diff --git a/src/objects/config/__init__.py b/src/objects/setup_configuration/__init__.py similarity index 100% rename from src/objects/config/__init__.py rename to src/objects/setup_configuration/__init__.py diff --git a/src/objects/config/models.py b/src/objects/setup_configuration/models/objecttypes.py similarity index 100% rename from src/objects/config/models.py rename to src/objects/setup_configuration/models/objecttypes.py diff --git a/src/objects/config/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py similarity index 95% rename from src/objects/config/objecttypes.py rename to src/objects/setup_configuration/steps/objecttypes.py index 6dc58c76..aad58993 100644 --- a/src/objects/config/objecttypes.py +++ b/src/objects/setup_configuration/steps/objecttypes.py @@ -4,7 +4,7 @@ from django_setup_configuration.exceptions import ConfigurationRunFailed from zgw_consumers.models import Service -from objects.config.models import ObjectTypesConfigurationModel +from objects.setup_configuration.models.objecttypes import ObjectTypesConfigurationModel from objects.core.models import ObjectType diff --git a/src/objects/tests/config/test_demo_configuration.py b/src/objects/tests/config/test_demo_configuration.py deleted file mode 100644 index 5d9a2cce..00000000 --- a/src/objects/tests/config/test_demo_configuration.py +++ /dev/null @@ -1,73 +0,0 @@ -from unittest.mock import patch - -from django.test import TestCase, override_settings - -import requests -import requests_mock -from django_setup_configuration.exceptions import SelfTestFailed - -from objects.config.demo import DemoUserStep -from objects.token.models import TokenAuth - - -@override_settings( - DEMO_TOKEN="demo-random-string", DEMO_PERSON="Demo", DEMO_EMAIL="demo@demo.local" -) -class DemoConfigurationTests(TestCase): - def test_configure(self): - configuration = DemoUserStep() - - configuration.configure() - - token_auth = TokenAuth.objects.get() - self.assertEqual(token_auth.token, "demo-random-string") - self.assertTrue(token_auth.is_superuser) - self.assertEqual(token_auth.contact_person, "Demo") - self.assertEqual(token_auth.email, "demo@demo.local") - - @requests_mock.Mocker() - @patch( - "objects.config.demo.build_absolute_url", - return_value="http://testserver/objects", - ) - def test_configuration_check_ok(self, m, *mocks): - configuration = DemoUserStep() - configuration.configure() - m.get("http://testserver/objects", json=[]) - - configuration.test_configuration() - - self.assertEqual(m.last_request.url, "http://testserver/objects") - self.assertEqual(m.last_request.method, "GET") - - @requests_mock.Mocker() - @patch( - "objects.config.demo.build_absolute_url", - return_value="http://testserver/objects", - ) - def test_configuration_check_failures(self, m, *mocks): - configuration = DemoUserStep() - configuration.configure() - - mock_kwargs = ( - {"exc": requests.ConnectTimeout}, - {"exc": requests.ConnectionError}, - {"status_code": 404}, - {"status_code": 403}, - {"status_code": 500}, - ) - for mock_config in mock_kwargs: - with self.subTest(mock=mock_config): - m.get("http://testserver/objects", **mock_config) - - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() - - def test_is_configured(self): - configuration = DemoUserStep() - - self.assertFalse(configuration.is_configured()) - - configuration.configure() - - self.assertTrue(configuration.is_configured()) diff --git a/src/objects/tests/config/test_objecttypes_configuration.py b/src/objects/tests/config/test_objecttypes_configuration.py deleted file mode 100644 index 37f8c336..00000000 --- a/src/objects/tests/config/test_objecttypes_configuration.py +++ /dev/null @@ -1,80 +0,0 @@ -from django.test import TestCase, override_settings - -import requests -import requests_mock -from django_setup_configuration.exceptions import SelfTestFailed -from zgw_consumers.constants import AuthTypes -from zgw_consumers.models import Service - -from objects.config.objecttypes import ObjecttypesStep - -from ..utils import mock_service_oas_get - - -@override_settings( - OBJECTTYPES_API_ROOT="https://objecttypes.example.com/api/v2/", - OBJECTTYPES_API_OAS="https://objecttypes.example.com/api/v2/schema/openapi.yaml", - OBJECTS_OBJECTTYPES_TOKEN="some-random-string", -) -class ObjecttypesConfigurationTests(TestCase): - def test_configure(self): - configuration = ObjecttypesStep() - - configuration.configure() - - service = Service.objects.get( - api_root="https://objecttypes.example.com/api/v2/" - ) - self.assertEqual( - service.oas, "https://objecttypes.example.com/api/v2/schema/openapi.yaml" - ) - self.assertEqual(service.auth_type, AuthTypes.api_key) - self.assertEqual(service.header_key, "Authorization") - self.assertEqual(service.header_value, "Token some-random-string") - - @requests_mock.Mocker() - def test_selftest_ok(self, m): - configuration = ObjecttypesStep() - configuration.configure() - mock_service_oas_get( - m, "https://objecttypes.example.com/api/v2/", "objecttypes" - ) - m.get("https://objecttypes.example.com/api/v2/objecttypes", json={}) - - configuration.test_configuration() - - self.assertEqual( - m.last_request.url, "https://objecttypes.example.com/api/v2/objecttypes" - ) - - @requests_mock.Mocker() - def test_selftest_fail(self, m): - configuration = ObjecttypesStep() - configuration.configure() - mock_service_oas_get( - m, "https://objecttypes.example.com/api/v2/", "objecttypes" - ) - - mock_kwargs = ( - {"exc": requests.ConnectTimeout}, - {"exc": requests.ConnectionError}, - {"status_code": 404}, - {"status_code": 403}, - {"status_code": 500}, - ) - for mock_config in mock_kwargs: - with self.subTest(mock=mock_config): - m.get( - "https://objecttypes.example.com/api/v2/objecttypes", **mock_config - ) - - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() - - def test_is_configured(self): - configuration = ObjecttypesStep() - self.assertFalse(configuration.is_configured()) - - configuration.configure() - - self.assertTrue(configuration.is_configured()) diff --git a/src/objects/tests/config/test_site_configuration.py b/src/objects/tests/config/test_site_configuration.py deleted file mode 100644 index b6e47ecd..00000000 --- a/src/objects/tests/config/test_site_configuration.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.contrib.sites.models import Site -from django.test import TestCase, override_settings - -import requests -import requests_mock -from django_setup_configuration.exceptions import SelfTestFailed - -from objects.config.site import SiteConfigurationStep - - -@override_settings( - OBJECTS_DOMAIN="localhost:8000", - OBJECTS_ORGANIZATION="ACME", -) -class SiteConfigurationTests(TestCase): - def setUp(self): - super().setUp() - - self.addCleanup(Site.objects.clear_cache) - - def test_set_domain(self): - configuration = SiteConfigurationStep() - configuration.configure() - - site = Site.objects.get_current() - self.assertEqual(site.domain, "localhost:8000") - self.assertEqual(site.name, "Objects ACME") - - @requests_mock.Mocker() - def test_configuration_check_ok(self, m): - m.get("http://localhost:8000/", status_code=200) - configuration = SiteConfigurationStep() - configuration.configure() - - configuration.test_configuration() - - self.assertEqual(m.last_request.url, "http://localhost:8000/") - self.assertEqual(m.last_request.method, "GET") - - @requests_mock.Mocker() - def test_configuration_check_failures(self, m): - configuration = SiteConfigurationStep() - configuration.configure() - - mock_kwargs = ( - {"exc": requests.ConnectTimeout}, - {"exc": requests.ConnectionError}, - {"status_code": 404}, - {"status_code": 403}, - {"status_code": 500}, - ) - for mock_config in mock_kwargs: - with self.subTest(mock=mock_config): - m.get("http://localhost:8000/", **mock_config) - - with self.assertRaises(SelfTestFailed): - configuration.test_configuration() - - def test_is_configured(self): - configuration = SiteConfigurationStep() - - self.assertFalse(configuration.is_configured()) - - configuration.configure() - - self.assertTrue(configuration.is_configured()) From 09e092a672c6d753716fddbafae94f3cf9f31d66 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Fri, 6 Dec 2024 15:59:40 +0100 Subject: [PATCH 09/32] [#467] remove test override --- src/objects/core/tests/test_objecttype_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py index 9ecb86bb..2568ce74 100644 --- a/src/objects/core/tests/test_objecttype_config.py +++ b/src/objects/core/tests/test_objecttype_config.py @@ -1,7 +1,7 @@ from pathlib import Path from django.db.models import QuerySet -from django.test import TestCase, override_settings +from django.test import TestCase from django_setup_configuration.exceptions import ConfigurationRunFailed from django_setup_configuration.test_utils import execute_single_step @@ -15,7 +15,6 @@ TEST_FILES = (Path(__file__).parent / "files").resolve() -@override_settings(ZGW_CONSUMERS_IGNORE_OAS_FIELDS=True) class ObjectTypesConfigurationStepTests(TestCase): def test_empty_database(self): service_1 = ServiceFactory(slug="service-1") From 8f60c305e6c4ab2613e74493606d354f5b040b7d Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Tue, 10 Dec 2024 10:27:20 +0100 Subject: [PATCH 10/32] [#467] update django-setup-configuration site step --- docs/installation/config_cli.rst | 33 +++--- src/objects/conf/base.py | 1 + .../setup_configuration/models/sites.py | 17 +++ .../setup_configuration/steps/sites.py | 40 +++++++ .../setup_configuration/tests/__init__.py | 0 .../tests/files/sites_empty_database.yaml | 8 ++ .../tests/files/sites_existing_sites.yaml | 8 ++ .../tests/files/sites_idempotent_step.yaml | 8 ++ .../tests/files/sites_invalid_domain.yaml | 8 ++ .../tests/test_site_config.py | 101 ++++++++++++++++++ 10 files changed, 208 insertions(+), 16 deletions(-) create mode 100644 src/objects/setup_configuration/models/sites.py create mode 100644 src/objects/setup_configuration/steps/sites.py create mode 100644 src/objects/setup_configuration/tests/__init__.py create mode 100644 src/objects/setup_configuration/tests/files/sites_empty_database.yaml create mode 100644 src/objects/setup_configuration/tests/files/sites_existing_sites.yaml create mode 100644 src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml create mode 100644 src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml create mode 100644 src/objects/setup_configuration/tests/test_site_config.py diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 0f733163..f2ebef1d 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -35,16 +35,29 @@ Objects API Sites configuration ------------------- -.. - _TODO: +Create or update a (single) YAML configuration file with your settings: + +.. code-block:: yaml + ... + sites_config_enable: true + sites: + items: + - domain: example.com + name: Example site + + - domain: test.example.com + name: Test site + ... +.. note:: The ``domain`` field will be used to lookup existing ``Site``'s. Objecttypes configuration ------------------------- -Create a (single) YAML configuration file with your settings: +Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml + ... objecttypes_config_enable: true objecttypes: items: @@ -55,6 +68,7 @@ Create a (single) YAML configuration file with your settings: - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 name: Object Type 2 service_identifier: service-2 + ... .. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. @@ -65,12 +79,6 @@ Demo user configuration _TODO: -Sites configuration -------------------- - -.. - _TODO: - Objects configuration --------------------- @@ -79,13 +87,6 @@ Objects configuration _TODO: -Demo user configuration ------------------------ - -.. - _TODO: - - Execution ========= diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 87df16bf..201c4f70 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -85,5 +85,6 @@ # SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", + "objects.setup_configuration.steps.sites.SitesConfigurationStep", "objects.setup_configuration.steps.objecttypes.ObjectTypesConfigurationStep", ) diff --git a/src/objects/setup_configuration/models/sites.py b/src/objects/setup_configuration/models/sites.py new file mode 100644 index 00000000..3ecdeee6 --- /dev/null +++ b/src/objects/setup_configuration/models/sites.py @@ -0,0 +1,17 @@ +from django.contrib.sites.models import Site +from django_setup_configuration.models import ConfigurationModel +from pydantic import Field + + +class SiteConfigurationModel(ConfigurationModel): + class Meta: + django_model_refs = { + Site: ( + "domain", + "name", + ) + } + + +class SitesConfigurationModel(ConfigurationModel): + items: list[SiteConfigurationModel] = Field() diff --git a/src/objects/setup_configuration/steps/sites.py b/src/objects/setup_configuration/steps/sites.py new file mode 100644 index 00000000..5ed62f29 --- /dev/null +++ b/src/objects/setup_configuration/steps/sites.py @@ -0,0 +1,40 @@ +from django.contrib.sites.models import Site + +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import ConfigurationRunFailed + +from objects.setup_configuration.models.sites import SitesConfigurationModel + + +class SitesConfigurationStep(BaseConfigurationStep): + config_model = SitesConfigurationModel + verbose_name = "Sites configuration" + + namespace = "sites" + enable_setting = "sites_config_enable" + + def execute(self, model: SitesConfigurationModel) -> None: + for item in model.items: + site_kwargs = dict(domain=item.domain, name=item.name) + site_instance = Site(**site_kwargs) + + try: + site_instance.full_clean(exclude=("id",), validate_unique=False) + except ValidationError as exception: + exception_message = ( + f"Validation error(s) occured for site {item.domain}." + ) + raise ConfigurationRunFailed(exception_message) from exception + + try: + Site.objects.update_or_create( + domain=item.domain, + defaults=dict(name=item.name) + ) + except IntegrityError as exception: + exception_message = ( + f"Failed configuring site {item.domain}." + ) + raise ConfigurationRunFailed(exception_message) from exception diff --git a/src/objects/setup_configuration/tests/__init__.py b/src/objects/setup_configuration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml new file mode 100644 index 00000000..808bdf57 --- /dev/null +++ b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml @@ -0,0 +1,8 @@ +sites_config_enable: true +sites: + items: + - domain: example.com + name: Example site + + - domain: alternative.example.com + name: Alternative example site diff --git a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml new file mode 100644 index 00000000..d2f00fbb --- /dev/null +++ b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml @@ -0,0 +1,8 @@ +sites_config_enable: true +sites: + items: + - domain: example.com + name: Example site (revised) + + - domain: test.example.com + name: Test site diff --git a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml new file mode 100644 index 00000000..808bdf57 --- /dev/null +++ b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml @@ -0,0 +1,8 @@ +sites_config_enable: true +sites: + items: + - domain: example.com + name: Example site + + - domain: alternative.example.com + name: Alternative example site diff --git a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml new file mode 100644 index 00000000..e8995718 --- /dev/null +++ b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml @@ -0,0 +1,8 @@ +sites_config_enable: true +sites: + items: + - domain: example.com + name: Example site + + - domain: foobar whitespace.com + name: Invalid site diff --git a/src/objects/setup_configuration/tests/test_site_config.py b/src/objects/setup_configuration/tests/test_site_config.py new file mode 100644 index 00000000..0c07189b --- /dev/null +++ b/src/objects/setup_configuration/tests/test_site_config.py @@ -0,0 +1,101 @@ +from pathlib import Path + +from django.contrib.sites.models import Site +from django.db.models import QuerySet +from django.test import TestCase + +from django_setup_configuration.exceptions import ConfigurationRunFailed +from django_setup_configuration.test_utils import execute_single_step + +from objects.setup_configuration.steps.sites import SitesConfigurationStep + +TEST_FILES = (Path(__file__).parent / "files").resolve() + + +class SitesConfigurationStepTests(TestCase): + def test_empty_database(self): + test_file_path = str(TEST_FILES / "sites_empty_database.yaml") + + execute_single_step(SitesConfigurationStep, yaml_source=test_file_path) + + sites: QuerySet[Site] = Site.objects.all() + + self.assertEqual(sites.count(), 2) + + example_site: Site = sites.get(name="Example site") + self.assertEqual(example_site.domain, "example.com") + + alternative_site: Site = sites.get(name="Alternative example site") + self.assertEqual(alternative_site.domain, "alternative.example.com") + + def test_existing_sites(self): + test_file_path = str(TEST_FILES / "sites_existing_sites.yaml") + + example_site, _ = Site.objects.get_or_create( + domain="example.com", + defaults=dict(name="Example site") + ) + + alternative_site = Site.objects.create( + domain="alternative.example.com", + name="Alternative example site", + ) + + execute_single_step(SitesConfigurationStep, yaml_source=test_file_path) + + sites: QuerySet[Site] = Site.objects.order_by("name") + + self.assertEqual(sites.count(), 3) + + example_site: Site = sites.get(name="Example site (revised)") + self.assertEqual(example_site.domain, "example.com") + + alternative_site: Site = sites.get(name="Alternative example site") + self.assertEqual(alternative_site.domain, "alternative.example.com") + + test_site: Site = sites.get(name="Test site") + self.assertEqual(test_site.domain, "test.example.com") + + def test_invalid_domain(self): + test_file_path = str(TEST_FILES / "sites_invalid_domain.yaml") + + with self.assertRaises(ConfigurationRunFailed): + execute_single_step(SitesConfigurationStep, yaml_source=test_file_path) + + sites: QuerySet[Site] = Site.objects.all() + + # the default test site created during test runs + self.assertEqual(sites.count(), 1) + + site: Site = sites.get() + + self.assertEqual(site.domain, "example.com") + + def test_idempotent_step(self): + test_file_path = str(TEST_FILES / "sites_idempotent_step.yaml") + + execute_single_step(SitesConfigurationStep, yaml_source=test_file_path) + + sites: QuerySet[Site] = Site.objects.all() + + self.assertEqual(sites.count(), 2) + + example_site: Site = sites.get(name="Example site") + self.assertEqual(example_site.domain, "example.com") + + alternative_site: Site = sites.get(name="Alternative example site") + self.assertEqual(alternative_site.domain, "alternative.example.com") + + execute_single_step(SitesConfigurationStep, yaml_source=test_file_path) + + self.assertEqual(Site.objects.count(), 2) + + example_site.refresh_from_db() + + self.assertEqual(example_site.name, "Example site") + self.assertEqual(example_site.domain, "example.com") + + alternative_site.refresh_from_db() + + self.assertEqual(alternative_site.name, "Alternative example site") + self.assertEqual(alternative_site.domain, "alternative.example.com") From f91b37d3fd1044fbb2ca9622da422f80d95bb94f Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Tue, 10 Dec 2024 11:49:37 +0100 Subject: [PATCH 11/32] [#467] remove demo user documentation This will be replaced by https://github.com/maykinmedia/objects-api/issues/485 --- docs/installation/config_cli.rst | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index f2ebef1d..4c77b0c3 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -73,20 +73,6 @@ Create or update a (single) YAML configuration file with your settings: .. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. -Demo user configuration ------------------------ -.. - _TODO: - - - -Objects configuration ---------------------- - -.. - _TODO: - - Execution ========= From 43964bdcbab5264af3cd58f508ad887ee2aab3a8 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Tue, 10 Dec 2024 11:56:18 +0100 Subject: [PATCH 12/32] [#467] apply code formatting --- src/objects/core/models.py | 2 +- .../core/tests/test_objecttype_config.py | 60 +++++++------------ .../setup_configuration/models/objecttypes.py | 8 +-- .../setup_configuration/models/sites.py | 1 + .../setup_configuration/steps/objecttypes.py | 12 ++-- .../setup_configuration/steps/sites.py | 9 +-- .../tests/test_site_config.py | 3 +- 7 files changed, 35 insertions(+), 60 deletions(-) diff --git a/src/objects/core/models.py b/src/objects/core/models.py index 3df3af1f..1a68df91 100644 --- a/src/objects/core/models.py +++ b/src/objects/core/models.py @@ -1,6 +1,6 @@ import datetime -from typing import Iterable, Optional import uuid +from typing import Iterable, Optional from django.contrib.gis.db.models import GeometryField from django.core.exceptions import ValidationError diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py index 2568ce74..5fb40bb5 100644 --- a/src/objects/core/tests/test_objecttype_config.py +++ b/src/objects/core/tests/test_objecttype_config.py @@ -30,17 +30,13 @@ def test_empty_database(self): objecttype_1: ObjectType = objecttypes.first() - self.assertEqual( - str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype_1._name, "Object Type 1") self.assertEqual(objecttype_1.service, service_1) objecttype_2: ObjectType = objecttypes.last() - self.assertEqual( - str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" - ) + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") self.assertEqual(objecttype_2._name, "Object Type 2") self.assertEqual(objecttype_2.service, service_2) @@ -53,12 +49,12 @@ def test_existing_objecttype(self): objecttype_1: ObjectType = ObjectTypeFactory( service=service_1, uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", - _name="Object Type 001" + _name="Object Type 001", ) objecttype_2: ObjectType = ObjectTypeFactory( service=service_2, uuid="b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2", - _name="Object Type 002" + _name="Object Type 002", ) execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) @@ -67,17 +63,13 @@ def test_existing_objecttype(self): objecttype_1.refresh_from_db() - self.assertEqual( - str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype_1._name, "Object Type 1") self.assertEqual(objecttype_1.service, service_1) objecttype_2.refresh_from_db() - self.assertEqual( - str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" - ) + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") self.assertEqual(objecttype_2._name, "Object Type 002") self.assertEqual(objecttype_2.service, service_2) @@ -85,9 +77,7 @@ def test_existing_objecttype(self): uuid="7229549b-7b41-47d1-8106-414b2a69751b" ) - self.assertEqual( - str(objecttype_3.uuid), "7229549b-7b41-47d1-8106-414b2a69751b" - ) + self.assertEqual(str(objecttype_3.uuid), "7229549b-7b41-47d1-8106-414b2a69751b") self.assertEqual(objecttype_3._name, "Object Type 3") self.assertEqual(objecttype_3.service, service_2) @@ -97,21 +87,21 @@ def test_unknown_service(self): objecttype: ObjectType = ObjectTypeFactory( uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", _name="Object Type 001", - service=service + service=service, ) test_file_path = str(TEST_FILES / "objecttypes_unknown_service.yaml") with self.assertRaises(ConfigurationRunFailed): - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + execute_single_step( + ObjectTypesConfigurationStep, yaml_source=test_file_path + ) self.assertEqual(ObjectType.objects.count(), 1) objecttype.refresh_from_db() - self.assertEqual( - str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype._name, "Object Type 001") self.assertEqual(objecttype.service, service) @@ -123,19 +113,19 @@ def test_invalid_uuid(self): objecttype: ObjectType = ObjectTypeFactory( service=service, uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", - _name="Object Type 001" + _name="Object Type 001", ) with self.assertRaises(ConfigurationRunFailed): - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + execute_single_step( + ObjectTypesConfigurationStep, yaml_source=test_file_path + ) self.assertEqual(ObjectType.objects.count(), 1) objecttype.refresh_from_db() - self.assertEqual( - str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype._name, "Object Type 1") self.assertEqual(objecttype.service, service) @@ -153,17 +143,13 @@ def test_idempotent_step(self): objecttype_1: ObjectType = objecttypes.first() - self.assertEqual( - str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype_1._name, "Object Type 1") self.assertEqual(objecttype_1.service, service_1) objecttype_2: ObjectType = objecttypes.last() - self.assertEqual( - str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" - ) + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") self.assertEqual(objecttype_2._name, "Object Type 2") self.assertEqual(objecttype_2.service, service_2) @@ -176,15 +162,11 @@ def test_idempotent_step(self): self.assertEqual(ObjectType.objects.count(), 2) # objecttype 1 - self.assertEqual( - str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281" - ) + self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") self.assertEqual(objecttype_1._name, "Object Type 1") self.assertEqual(objecttype_1.service, service_1) # objecttype 2 - self.assertEqual( - str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2" - ) + self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") self.assertEqual(objecttype_2._name, "Object Type 2") self.assertEqual(objecttype_2.service, service_2) diff --git a/src/objects/setup_configuration/models/objecttypes.py b/src/objects/setup_configuration/models/objecttypes.py index 20714a2a..0f440d33 100644 --- a/src/objects/setup_configuration/models/objecttypes.py +++ b/src/objects/setup_configuration/models/objecttypes.py @@ -1,7 +1,7 @@ from django_setup_configuration.fields import DjangoModelRef from django_setup_configuration.models import ConfigurationModel -from zgw_consumers.models import Service from pydantic import Field +from zgw_consumers.models import Service from objects.core.models import ObjectType @@ -11,11 +11,7 @@ class ObjectTypeConfigurationModel(ConfigurationModel): name: str = DjangoModelRef(ObjectType, "_name") class Meta: - django_model_refs = { - ObjectType: ( - "uuid", - ) - } + django_model_refs = {ObjectType: ("uuid",)} class ObjectTypesConfigurationModel(ConfigurationModel): diff --git a/src/objects/setup_configuration/models/sites.py b/src/objects/setup_configuration/models/sites.py index 3ecdeee6..347d8e0d 100644 --- a/src/objects/setup_configuration/models/sites.py +++ b/src/objects/setup_configuration/models/sites.py @@ -1,4 +1,5 @@ from django.contrib.sites.models import Site + from django_setup_configuration.models import ConfigurationModel from pydantic import Field diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py index aad58993..91914ac4 100644 --- a/src/objects/setup_configuration/steps/objecttypes.py +++ b/src/objects/setup_configuration/steps/objecttypes.py @@ -1,11 +1,12 @@ from django.core.exceptions import ValidationError from django.db import IntegrityError + from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed from zgw_consumers.models import Service -from objects.setup_configuration.models.objecttypes import ObjectTypesConfigurationModel from objects.core.models import ObjectType +from objects.setup_configuration.models.objecttypes import ObjectTypesConfigurationModel class ObjectTypesConfigurationStep(BaseConfigurationStep): @@ -46,12 +47,11 @@ def execute(self, model: ObjectTypesConfigurationModel) -> None: ObjectType.objects.update_or_create( uuid=item.uuid, defaults={ - key: value for key, value in objecttype_kwargs.items() + key: value + for key, value in objecttype_kwargs.items() if key != "uuid" - } + }, ) except IntegrityError as exception: - exception_message = ( - f"Failed configuring ObjectType {item.uuid}." - ) + exception_message = f"Failed configuring ObjectType {item.uuid}." raise ConfigurationRunFailed(exception_message) from exception diff --git a/src/objects/setup_configuration/steps/sites.py b/src/objects/setup_configuration/steps/sites.py index 5ed62f29..199e38f2 100644 --- a/src/objects/setup_configuration/steps/sites.py +++ b/src/objects/setup_configuration/steps/sites.py @@ -1,7 +1,7 @@ from django.contrib.sites.models import Site - from django.core.exceptions import ValidationError from django.db import IntegrityError + from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed @@ -30,11 +30,8 @@ def execute(self, model: SitesConfigurationModel) -> None: try: Site.objects.update_or_create( - domain=item.domain, - defaults=dict(name=item.name) + domain=item.domain, defaults=dict(name=item.name) ) except IntegrityError as exception: - exception_message = ( - f"Failed configuring site {item.domain}." - ) + exception_message = f"Failed configuring site {item.domain}." raise ConfigurationRunFailed(exception_message) from exception diff --git a/src/objects/setup_configuration/tests/test_site_config.py b/src/objects/setup_configuration/tests/test_site_config.py index 0c07189b..3dd2f05b 100644 --- a/src/objects/setup_configuration/tests/test_site_config.py +++ b/src/objects/setup_configuration/tests/test_site_config.py @@ -32,8 +32,7 @@ def test_existing_sites(self): test_file_path = str(TEST_FILES / "sites_existing_sites.yaml") example_site, _ = Site.objects.get_or_create( - domain="example.com", - defaults=dict(name="Example site") + domain="example.com", defaults=dict(name="Example site") ) alternative_site = Site.objects.create( From 3461fcaa9874016a0da21930fb60c8f56d1ab688 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Tue, 10 Dec 2024 12:06:25 +0100 Subject: [PATCH 13/32] [#467] include service configuration documentation --- docs/installation/config_cli.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 4c77b0c3..46022516 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -58,20 +58,38 @@ Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml ... + zgw_consumers_config_enable: true + zgw_consumers: + services: + - identifier: objecttypen-foo + label: Objecttypen API Foo + api_root: http://objecttypen.foo/api/v1/ + api_type: orc + auth_type: api_key + - identifier: objecttypen-bar + label: Objecttypen API Bar + api_root: http://objecttypen.bar/api/v1/ + api_type: orc + auth_type: api_key + objecttypes_config_enable: true objecttypes: items: - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 name: Object Type 1 - service_identifier: service-1 + service_identifier: objecttypen-foo - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 name: Object Type 2 - service_identifier: service-2 + service_identifier: objecttypen-bar ... .. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. +Objecttypes require a corresponding ``Service`` to work correctly. Creating +these ``Service``'s can be done by defining these in the same yaml file. ``Service`` +instances will be created before the ``ObjectType``'s are created. + Execution ========= From c5cd151c019f1bff3a5729fb3ad0bc94c76c9b22 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 11:15:32 +0100 Subject: [PATCH 14/32] [#468] Add identifier in TokenAuth and create migrations --- src/objects/token/admin.py | 2 +- ...enauth_identifier_alter_tokenauth_token.py | 70 +++++++++++++++++++ src/objects/token/models.py | 13 +++- src/objects/token/tests/test_validators.py | 45 ++++++++++++ src/objects/token/validators.py | 18 +++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/objects/token/migrations/0017_tokenauth_identifier_alter_tokenauth_token.py create mode 100644 src/objects/token/tests/test_validators.py create mode 100644 src/objects/token/validators.py diff --git a/src/objects/token/admin.py b/src/objects/token/admin.py index 3d7d82b6..7898b30d 100644 --- a/src/objects/token/admin.py +++ b/src/objects/token/admin.py @@ -162,7 +162,7 @@ def get_uuid(self, obj): @admin.register(TokenAuth) class TokenAuthAdmin(admin.ModelAdmin): list_display = ( - "token", + "identifier", "contact_person", "organization", "administration", diff --git a/src/objects/token/migrations/0017_tokenauth_identifier_alter_tokenauth_token.py b/src/objects/token/migrations/0017_tokenauth_identifier_alter_tokenauth_token.py new file mode 100644 index 00000000..780aa27c --- /dev/null +++ b/src/objects/token/migrations/0017_tokenauth_identifier_alter_tokenauth_token.py @@ -0,0 +1,70 @@ +# Generated by Django 4.2.15 on 2024-12-11 10:07 +import logging + +from django.db import migrations, models +from django.db.migrations.state import StateApps + +import objects.token.validators + +logger = logging.getLogger(__name__) + + +def _generate_unique_identifiers(apps: StateApps, schema_editor) -> None: + TokenAuth = apps.get_model("token", "TokenAuth") + + count = 1 + + for token in TokenAuth.objects.filter(identifier__isnull=True): + while TokenAuth.objects.filter(identifier=f"token-{count}").exists(): + count += 1 + + identifier = f"token-{count}" + logger.debug(f"Generated {identifier} for token {token.pk}") + + token.identifier = identifier + token.save(update_fields=("identifier",)) + + +class Migration(migrations.Migration): + + dependencies = [ + ("token", "0016_alter_permission_token_auth"), + ] + + operations = [ + migrations.AddField( + model_name="tokenauth", + name="identifier", + field=models.CharField( + blank=True, + null=True, + ), + ), + migrations.RunPython( + code=_generate_unique_identifiers, + reverse_code=migrations.RunPython.noop, + ), + migrations.AlterField( + model_name="tokenauth", + name="identifier", + field=models.SlugField(unique=True), + ), + migrations.AlterField( + model_name="tokenauth", + name="identifier", + field=models.SlugField( + help_text="A human-friendly label to refer to this token", + unique=True, + ), + ), + migrations.AlterField( + model_name="tokenauth", + name="token", + field=models.CharField( + max_length=40, + unique=True, + validators=[objects.token.validators.validate_whitespace], + verbose_name="token", + ), + ), + ] diff --git a/src/objects/token/models.py b/src/objects/token/models.py index a11a7b2e..5731d590 100644 --- a/src/objects/token/models.py +++ b/src/objects/token/models.py @@ -9,9 +9,20 @@ from .constants import PermissionModes +from objects.token.validators import validate_whitespace + class TokenAuth(models.Model): - token = models.CharField(_("token"), max_length=40, unique=True) + identifier = models.SlugField( + unique=True, + help_text=_("A human-friendly label to refer to this token"), + ) + token = models.CharField( + _("token"), + max_length=40, + unique=True, + validators=[validate_whitespace], + ) contact_person = models.CharField( _("contact person"), max_length=200, diff --git a/src/objects/token/tests/test_validators.py b/src/objects/token/tests/test_validators.py new file mode 100644 index 00000000..2f00a26d --- /dev/null +++ b/src/objects/token/tests/test_validators.py @@ -0,0 +1,45 @@ +from django.core.exceptions import ValidationError +from django.test import SimpleTestCase + +from objects.token.validators import validate_whitespace + + +class WhiteSpaceValidatorTestCase(SimpleTestCase): + def test_characters_only(self): + self.assertIsNone(validate_whitespace("test123")) + + def test_trailing_whitespace(self): + with self.assertRaises(ValidationError): + validate_whitespace("test123 ") + + def test_leading_whitespace(self): + with self.assertRaises(ValidationError): + validate_whitespace(" test123") + + def test_whitespace_in_between(self): + with self.assertRaises(ValidationError): + validate_whitespace("test 123") + + def test_whitespace_only(self): + with self.assertRaises(ValidationError): + validate_whitespace(" ") + + def test_trailing_tab_character(self): + with self.assertRaises(ValidationError): + validate_whitespace("test123\t") + + def test_leading_tab_character(self): + with self.assertRaises(ValidationError): + validate_whitespace("\ttest123") + + def test_tab_character_in_between(self): + with self.assertRaises(ValidationError): + validate_whitespace("test\t123") + + def test_tab_characters_only(self): + with self.assertRaises(ValidationError): + validate_whitespace("\t\t") + + def test_blank_value(self): + with self.assertRaises(ValidationError): + validate_whitespace("") diff --git a/src/objects/token/validators.py b/src/objects/token/validators.py new file mode 100644 index 00000000..a8fc2285 --- /dev/null +++ b/src/objects/token/validators.py @@ -0,0 +1,18 @@ +import re + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext as _ + +# includes tabs, carriage returns, newlines, form-feeds and vertical whitespace characters +WHITESPACE_PATTERN = re.compile(r".*\s.*") + + +def validate_whitespace(value: str) -> None: + if not value: + raise ValidationError(code="invalid", message=_("Blank values are not allowed")) + + if WHITESPACE_PATTERN.match(value): + raise ValidationError( + code="all-whitespace", + message=_("Tokens cannot contain whitespace-like characters"), + ) From 4a256d931f62dabce042114b3cc303aab2940ca6 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 11:31:55 +0100 Subject: [PATCH 15/32] [#467] Update objecttypes configuration step The step now lets you configure an connection with an objecttypes api --- docs/installation/config_cli.rst | 43 +-- src/objects/conf/base.py | 2 +- .../tests/files/objecttypes_connection.yaml | 16 ++ .../files/objecttypes_connection_invalid.yaml | 16 ++ .../files/objecttypes_empty_database.yaml | 10 - .../objecttypes_existing_objecttype.yaml | 10 - .../tests/files/objecttypes_idempotent.yaml | 10 - .../tests/files/objecttypes_invalid_uuid.yaml | 10 - .../files/objecttypes_unknown_service.yaml | 10 - .../core/tests/test_objecttype_config.py | 255 ++++++++---------- .../setup_configuration/models/objecttypes.py | 18 -- .../setup_configuration/steps/objecttypes.py | 92 +++---- 12 files changed, 196 insertions(+), 296 deletions(-) create mode 100644 src/objects/core/tests/files/objecttypes_connection.yaml create mode 100644 src/objects/core/tests/files/objecttypes_connection_invalid.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_empty_database.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_existing_objecttype.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_idempotent.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_invalid_uuid.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_unknown_service.yaml delete mode 100644 src/objects/setup_configuration/models/objecttypes.py diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 46022516..6ec0c940 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -51,44 +51,23 @@ Create or update a (single) YAML configuration file with your settings: .. note:: The ``domain`` field will be used to lookup existing ``Site``'s. -Objecttypes configuration +Objecttypes connection configuration ------------------------- Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml + objecttypes_connection_config_enable: true + objecttypes_connection: + identifier: objecttypen + label: ObjectTypen API + api_root: http://objecttypen.nl/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + header_key: Authorization + header_value: Token foo ... - zgw_consumers_config_enable: true - zgw_consumers: - services: - - identifier: objecttypen-foo - label: Objecttypen API Foo - api_root: http://objecttypen.foo/api/v1/ - api_type: orc - auth_type: api_key - - identifier: objecttypen-bar - label: Objecttypen API Bar - api_root: http://objecttypen.bar/api/v1/ - api_type: orc - auth_type: api_key - - objecttypes_config_enable: true - objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: objecttypen-foo - - - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 - name: Object Type 2 - service_identifier: objecttypen-bar - ... - -.. note:: The ``uuid`` field will be used to lookup existing ``ObjectType``'s. - -Objecttypes require a corresponding ``Service`` to work correctly. Creating -these ``Service``'s can be done by defining these in the same yaml file. ``Service`` -instances will be created before the ``ObjectType``'s are created. Execution diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 201c4f70..09be2714 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -86,5 +86,5 @@ SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", "objects.setup_configuration.steps.sites.SitesConfigurationStep", - "objects.setup_configuration.steps.objecttypes.ObjectTypesConfigurationStep", + "objects.setup_configuration.steps.objecttypes.ObjectTypesConnectionConfigurationStep", ) diff --git a/src/objects/core/tests/files/objecttypes_connection.yaml b/src/objects/core/tests/files/objecttypes_connection.yaml new file mode 100644 index 00000000..d52b4ed9 --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_connection.yaml @@ -0,0 +1,16 @@ +objecttypes_connection_config_enable: true +objecttypes_connection: + identifier: objecttypes-api + label: Objecttypen API + api_root: https://objecttypes.nl/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: zgw + header_key: Authorization + header_value: Token foo + client_id: client + secret: secret + nlx: http://some-outway-adress.local:8080/ + user_id: objects-api + user_representation: Objects API + timeout: 60 diff --git a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml new file mode 100644 index 00000000..164deaa7 --- /dev/null +++ b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml @@ -0,0 +1,16 @@ +objecttypes_connection_config_enable: true +objecttypes_connection: + identifier: identifier with whitespace in between + label: Objecttypen API + api_root: https://objecttypes.nl/api/v1/ + api_connection_check_path: objectttypes + api_type: orc + auth_type: zgw + header_key: Authorization + header_value: Token foo + client_id: client + secret: super-secret + nlx: http://some-outway-adress.local:8080/ + user_id: objects-api + user_representation: Objects API + timeout: 60 diff --git a/src/objects/core/tests/files/objecttypes_empty_database.yaml b/src/objects/core/tests/files/objecttypes_empty_database.yaml deleted file mode 100644 index b969949e..00000000 --- a/src/objects/core/tests/files/objecttypes_empty_database.yaml +++ /dev/null @@ -1,10 +0,0 @@ -objecttypes_config_enable: true -objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: service-1 - - - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 - name: Object Type 2 - service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml b/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml deleted file mode 100644 index f93e005f..00000000 --- a/src/objects/core/tests/files/objecttypes_existing_objecttype.yaml +++ /dev/null @@ -1,10 +0,0 @@ -objecttypes_config_enable: true -objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: service-1 - - - uuid: 7229549b-7b41-47d1-8106-414b2a69751b - name: Object Type 3 - service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_idempotent.yaml b/src/objects/core/tests/files/objecttypes_idempotent.yaml deleted file mode 100644 index b969949e..00000000 --- a/src/objects/core/tests/files/objecttypes_idempotent.yaml +++ /dev/null @@ -1,10 +0,0 @@ -objecttypes_config_enable: true -objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: service-1 - - - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 - name: Object Type 2 - service_identifier: service-2 diff --git a/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml b/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml deleted file mode 100644 index 2a360c8e..00000000 --- a/src/objects/core/tests/files/objecttypes_invalid_uuid.yaml +++ /dev/null @@ -1,10 +0,0 @@ -objecttypes_config_enable: true -objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: service-1 - - - uuid: foobar - name: Object Type 2 - service_identifier: service-1 diff --git a/src/objects/core/tests/files/objecttypes_unknown_service.yaml b/src/objects/core/tests/files/objecttypes_unknown_service.yaml deleted file mode 100644 index 8348427c..00000000 --- a/src/objects/core/tests/files/objecttypes_unknown_service.yaml +++ /dev/null @@ -1,10 +0,0 @@ -objecttypes_config_enable: true -objecttypes: - items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: unknown - - - uuid: b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2 - name: Object Type 2 - service_identifier: service-1 diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py index 5fb40bb5..706aef9f 100644 --- a/src/objects/core/tests/test_objecttype_config.py +++ b/src/objects/core/tests/test_objecttype_config.py @@ -1,172 +1,135 @@ from pathlib import Path -from django.db.models import QuerySet from django.test import TestCase -from django_setup_configuration.exceptions import ConfigurationRunFailed from django_setup_configuration.test_utils import execute_single_step +from zgw_consumers.constants import APITypes, AuthTypes from zgw_consumers.models import Service from zgw_consumers.test.factories import ServiceFactory -from objects.core.models import ObjectType -from objects.core.tests.factories import ObjectTypeFactory -from objects.setup_configuration.steps.objecttypes import ObjectTypesConfigurationStep +from objects.setup_configuration.steps.objecttypes import ( + ObjectTypesConnectionConfigurationStep, +) TEST_FILES = (Path(__file__).parent / "files").resolve() -class ObjectTypesConfigurationStepTests(TestCase): - def test_empty_database(self): - service_1 = ServiceFactory(slug="service-1") - service_2 = ServiceFactory(slug="service-2") +class ObjectTypesConnectionConfigurationStepTests(TestCase): + def test_create_connection(self): + test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") - test_file_path = str(TEST_FILES / "objecttypes_empty_database.yaml") - - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) - - objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") - - self.assertEqual(objecttypes.count(), 2) - - objecttype_1: ObjectType = objecttypes.first() - - self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype_1._name, "Object Type 1") - self.assertEqual(objecttype_1.service, service_1) - - objecttype_2: ObjectType = objecttypes.last() - - self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") - self.assertEqual(objecttype_2._name, "Object Type 2") - self.assertEqual(objecttype_2.service, service_2) - - def test_existing_objecttype(self): - test_file_path = str(TEST_FILES / "objecttypes_existing_objecttype.yaml") - - service_1: Service = ServiceFactory(slug="service-1") - service_2: Service = ServiceFactory(slug="service-2") - - objecttype_1: ObjectType = ObjectTypeFactory( - service=service_1, - uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", - _name="Object Type 001", - ) - objecttype_2: ObjectType = ObjectTypeFactory( - service=service_2, - uuid="b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2", - _name="Object Type 002", + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path ) - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) - - self.assertEqual(ObjectType.objects.count(), 3) - - objecttype_1.refresh_from_db() - - self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype_1._name, "Object Type 1") - self.assertEqual(objecttype_1.service, service_1) - - objecttype_2.refresh_from_db() - - self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") - self.assertEqual(objecttype_2._name, "Object Type 002") - self.assertEqual(objecttype_2.service, service_2) - - objecttype_3: ObjectType = ObjectType.objects.get( - uuid="7229549b-7b41-47d1-8106-414b2a69751b" + service = Service.objects.get(slug="objecttypes-api") + + self.assertEqual(service.label, "Objecttypen API") + self.assertEqual(service.api_type, APITypes.orc) + self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") + self.assertEqual(service.api_connection_check_path, "objecttypes") + self.assertEqual(service.auth_type, AuthTypes.zgw) + self.assertEqual(service.client_id, "client") + self.assertEqual(service.secret, "secret") + self.assertEqual(service.header_key, "Authorization") + self.assertEqual(service.header_value, "Token foo") + self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") + self.assertEqual(service.user_id, "objects-api") + self.assertEqual(service.user_representation, "Objects API") + self.assertEqual(service.timeout, 60) + + def test_update_connection(self): + test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") + + service: Service = ServiceFactory( + slug="objecttypes-api", + label="Objecttypen API Test", + api_type=APITypes.zrc, + api_root="https://test.objecttypes.nl/api/v1/", + api_connection_check_path="objecttype", + auth_type=AuthTypes.api_key, + client_id="test-client", + secret="test-secret", + header_key="authorization", + header_value="Token foobar", + nlx="http://test.some-outway-adress.local:8080/", + user_id="test-objects-api", + user_representation="Test Objects API", + timeout=30, ) - self.assertEqual(str(objecttype_3.uuid), "7229549b-7b41-47d1-8106-414b2a69751b") - self.assertEqual(objecttype_3._name, "Object Type 3") - self.assertEqual(objecttype_3.service, service_2) - - def test_unknown_service(self): - service = ServiceFactory(slug="service-1") - - objecttype: ObjectType = ObjectTypeFactory( - uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", - _name="Object Type 001", - service=service, + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path ) - test_file_path = str(TEST_FILES / "objecttypes_unknown_service.yaml") - - with self.assertRaises(ConfigurationRunFailed): - execute_single_step( - ObjectTypesConfigurationStep, yaml_source=test_file_path - ) - - self.assertEqual(ObjectType.objects.count(), 1) - - objecttype.refresh_from_db() - - self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype._name, "Object Type 001") - self.assertEqual(objecttype.service, service) - - def test_invalid_uuid(self): - test_file_path = str(TEST_FILES / "objecttypes_invalid_uuid.yaml") - - service: Service = ServiceFactory(slug="service-1") - - objecttype: ObjectType = ObjectTypeFactory( - service=service, - uuid="b427ef84-189d-43aa-9efd-7bb2c459e281", - _name="Object Type 001", + self.assertEqual(Service.objects.count(), 1) + + service.refresh_from_db() + + self.assertEqual(service.label, "Objecttypen API") + self.assertEqual(service.api_type, APITypes.orc) + self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") + self.assertEqual(service.api_connection_check_path, "objecttypes") + self.assertEqual(service.auth_type, AuthTypes.zgw) + self.assertEqual(service.client_id, "client") + self.assertEqual(service.secret, "secret") + self.assertEqual(service.header_key, "Authorization") + self.assertEqual(service.header_value, "Token foo") + self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") + self.assertEqual(service.user_id, "objects-api") + self.assertEqual(service.user_representation, "Objects API") + self.assertEqual(service.timeout, 60) + + def test_invalid_identifier(self): + test_file_path = str(TEST_FILES / "objecttypes_connection_invalid.yaml") + + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path ) - with self.assertRaises(ConfigurationRunFailed): - execute_single_step( - ObjectTypesConfigurationStep, yaml_source=test_file_path - ) - - self.assertEqual(ObjectType.objects.count(), 1) - - objecttype.refresh_from_db() - - self.assertEqual(str(objecttype.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype._name, "Object Type 1") - self.assertEqual(objecttype.service, service) + self.assertEqual(Service.objects.count(), 0) def test_idempotent_step(self): - service_1 = ServiceFactory(slug="service-1") - service_2 = ServiceFactory(slug="service-2") - - test_file_path = str(TEST_FILES / "objecttypes_idempotent.yaml") - - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) + test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") - objecttypes: QuerySet[ObjectType] = ObjectType.objects.order_by("_name") - - self.assertEqual(objecttypes.count(), 2) - - objecttype_1: ObjectType = objecttypes.first() - - self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype_1._name, "Object Type 1") - self.assertEqual(objecttype_1.service, service_1) - - objecttype_2: ObjectType = objecttypes.last() - - self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") - self.assertEqual(objecttype_2._name, "Object Type 2") - self.assertEqual(objecttype_2.service, service_2) - - # Rerun - execute_single_step(ObjectTypesConfigurationStep, yaml_source=test_file_path) - - objecttype_1.refresh_from_db() - objecttype_2.refresh_from_db() - - self.assertEqual(ObjectType.objects.count(), 2) + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path + ) - # objecttype 1 - self.assertEqual(str(objecttype_1.uuid), "b427ef84-189d-43aa-9efd-7bb2c459e281") - self.assertEqual(objecttype_1._name, "Object Type 1") - self.assertEqual(objecttype_1.service, service_1) + service = Service.objects.get(slug="objecttypes-api") + + self.assertEqual(service.label, "Objecttypen API") + self.assertEqual(service.api_type, APITypes.orc) + self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") + self.assertEqual(service.api_connection_check_path, "objecttypes") + self.assertEqual(service.auth_type, AuthTypes.zgw) + self.assertEqual(service.client_id, "client") + self.assertEqual(service.secret, "secret") + self.assertEqual(service.header_key, "Authorization") + self.assertEqual(service.header_value, "Token foo") + self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") + self.assertEqual(service.user_id, "objects-api") + self.assertEqual(service.user_representation, "Objects API") + self.assertEqual(service.timeout, 60) + + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path + ) - # objecttype 2 - self.assertEqual(str(objecttype_2.uuid), "b0e8553f-8b1a-4d55-ab90-6d02f1bcf2c2") - self.assertEqual(objecttype_2._name, "Object Type 2") - self.assertEqual(objecttype_2.service, service_2) + self.assertEqual(Service.objects.count(), 1) + + service.refresh_from_db() + + self.assertEqual(service.label, "Objecttypen API") + self.assertEqual(service.api_type, APITypes.orc) + self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") + self.assertEqual(service.api_connection_check_path, "objecttypes") + self.assertEqual(service.auth_type, AuthTypes.zgw) + self.assertEqual(service.client_id, "client") + self.assertEqual(service.secret, "secret") + self.assertEqual(service.header_key, "Authorization") + self.assertEqual(service.header_value, "Token foo") + self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") + self.assertEqual(service.user_id, "objects-api") + self.assertEqual(service.user_representation, "Objects API") + self.assertEqual(service.timeout, 60) diff --git a/src/objects/setup_configuration/models/objecttypes.py b/src/objects/setup_configuration/models/objecttypes.py deleted file mode 100644 index 0f440d33..00000000 --- a/src/objects/setup_configuration/models/objecttypes.py +++ /dev/null @@ -1,18 +0,0 @@ -from django_setup_configuration.fields import DjangoModelRef -from django_setup_configuration.models import ConfigurationModel -from pydantic import Field -from zgw_consumers.models import Service - -from objects.core.models import ObjectType - - -class ObjectTypeConfigurationModel(ConfigurationModel): - service_identifier: str = DjangoModelRef(Service, "slug") - name: str = DjangoModelRef(ObjectType, "_name") - - class Meta: - django_model_refs = {ObjectType: ("uuid",)} - - -class ObjectTypesConfigurationModel(ConfigurationModel): - items: list[ObjectTypeConfigurationModel] = Field() diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py index 91914ac4..2f2a2792 100644 --- a/src/objects/setup_configuration/steps/objecttypes.py +++ b/src/objects/setup_configuration/steps/objecttypes.py @@ -1,57 +1,51 @@ -from django.core.exceptions import ValidationError from django.db import IntegrityError from django_setup_configuration.configuration import BaseConfigurationStep from django_setup_configuration.exceptions import ConfigurationRunFailed +from zgw_consumers.contrib.setup_configuration.models import ( + SingleServiceConfigurationModel, +) from zgw_consumers.models import Service -from objects.core.models import ObjectType -from objects.setup_configuration.models.objecttypes import ObjectTypesConfigurationModel - -class ObjectTypesConfigurationStep(BaseConfigurationStep): - config_model = ObjectTypesConfigurationModel - verbose_name = "Objecttypes Configuration" - - namespace = "objecttypes" - enable_setting = "objecttypes_config_enable" - - def execute(self, model: ObjectTypesConfigurationModel) -> None: - for item in model.items: - try: - service = Service.objects.get(slug=item.service_identifier) - except Service.DoesNotExist: - raise ConfigurationRunFailed( - f"No service found with identifier {item.service_identifier}" - ) - - objecttype_kwargs = dict( - service=service, - uuid=item.uuid, - _name=item.name, +class ObjectTypesConnectionConfigurationStep( + BaseConfigurationStep[SingleServiceConfigurationModel] +): + config_model = SingleServiceConfigurationModel + verbose_name = "Objecttypes connection configuration" + + namespace = "objecttypes_connection" + enable_setting = "objecttypes_connection_config_enable" + + def execute(self, model: SingleServiceConfigurationModel) -> None: + service_kwargs = dict( + slug=model.identifier, + label=model.label, + api_type=model.api_type, + api_root=model.api_root, + api_connection_check_path=model.api_connection_check_path, + auth_type=model.auth_type, + client_id=model.client_id, + secret=model.secret, + header_key=model.header_key, + header_value=model.header_value, + nlx=model.nlx, + user_id=model.user_id, + user_representation=model.user_representation, + timeout=model.timeout, + ) + + service = Service(**service_kwargs) + + try: + Service.objects.update_or_create( + slug=service.slug, + defaults={ + key: value for key, value in service_kwargs.items() if key != "slug" + }, ) - - objecttype_instance = ObjectType(**objecttype_kwargs) - - try: - objecttype_instance.full_clean( - exclude=("id", "service"), validate_unique=False - ) - except ValidationError as exception: - exception_message = ( - f"Validation error(s) occured for objecttype {item.uuid}." - ) - raise ConfigurationRunFailed(exception_message) from exception - - try: - ObjectType.objects.update_or_create( - uuid=item.uuid, - defaults={ - key: value - for key, value in objecttype_kwargs.items() - if key != "uuid" - }, - ) - except IntegrityError as exception: - exception_message = f"Failed configuring ObjectType {item.uuid}." - raise ConfigurationRunFailed(exception_message) from exception + except IntegrityError as exception: + exception_message = ( + f"Failed configuring ObjectType connection {service.slug}." + ) + raise ConfigurationRunFailed(exception_message) from exception From fc99137f1e4041de9bf5136b6d3576186612d971 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 11:36:15 +0100 Subject: [PATCH 16/32] [#467] update config namespace --- docs/installation/config_cli.rst | 4 ++-- src/objects/core/tests/files/objecttypes_connection.yaml | 4 ++-- .../core/tests/files/objecttypes_connection_invalid.yaml | 4 ++-- src/objects/setup_configuration/steps/objecttypes.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 6ec0c940..68cd9dd4 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -57,8 +57,8 @@ Objecttypes connection configuration Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml - objecttypes_connection_config_enable: true - objecttypes_connection: + objects_api_objecttypes_connection_config_enable: true + objects_api_objecttypes_connection": identifier: objecttypen label: ObjectTypen API api_root: http://objecttypen.nl/api/v1/ diff --git a/src/objects/core/tests/files/objecttypes_connection.yaml b/src/objects/core/tests/files/objecttypes_connection.yaml index d52b4ed9..0cf3177f 100644 --- a/src/objects/core/tests/files/objecttypes_connection.yaml +++ b/src/objects/core/tests/files/objecttypes_connection.yaml @@ -1,5 +1,5 @@ -objecttypes_connection_config_enable: true -objecttypes_connection: +objects_api_objecttypes_connection_config_enable: true +objects_api_objecttypes_connection: identifier: objecttypes-api label: Objecttypen API api_root: https://objecttypes.nl/api/v1/ diff --git a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml index 164deaa7..c12d0270 100644 --- a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml +++ b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml @@ -1,5 +1,5 @@ -objecttypes_connection_config_enable: true -objecttypes_connection: +objects_api_objecttypes_connection_config_enable: true +objects_api_objecttypes_connection: identifier: identifier with whitespace in between label: Objecttypen API api_root: https://objecttypes.nl/api/v1/ diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py index 2f2a2792..e99f3061 100644 --- a/src/objects/setup_configuration/steps/objecttypes.py +++ b/src/objects/setup_configuration/steps/objecttypes.py @@ -14,8 +14,8 @@ class ObjectTypesConnectionConfigurationStep( config_model = SingleServiceConfigurationModel verbose_name = "Objecttypes connection configuration" - namespace = "objecttypes_connection" - enable_setting = "objecttypes_connection_config_enable" + namespace = "objects_api_objecttypes_connection" + enable_setting = "objects_api_objecttypes_connection_config_enable" def execute(self, model: SingleServiceConfigurationModel) -> None: service_kwargs = dict( From 951e53ff25f5a9f1aaec7d449d68955e442ee601 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 11:38:29 +0100 Subject: [PATCH 17/32] [#467] update sites config namespace --- docs/installation/config_cli.rst | 2 +- src/objects/setup_configuration/steps/sites.py | 2 +- .../setup_configuration/tests/files/sites_empty_database.yaml | 2 +- .../setup_configuration/tests/files/sites_existing_sites.yaml | 2 +- .../setup_configuration/tests/files/sites_idempotent_step.yaml | 2 +- .../setup_configuration/tests/files/sites_invalid_domain.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 68cd9dd4..8cf749c1 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -40,7 +40,7 @@ Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml ... sites_config_enable: true - sites: + sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/steps/sites.py b/src/objects/setup_configuration/steps/sites.py index 199e38f2..e92c5627 100644 --- a/src/objects/setup_configuration/steps/sites.py +++ b/src/objects/setup_configuration/steps/sites.py @@ -12,7 +12,7 @@ class SitesConfigurationStep(BaseConfigurationStep): config_model = SitesConfigurationModel verbose_name = "Sites configuration" - namespace = "sites" + namespace = "sites_config" enable_setting = "sites_config_enable" def execute(self, model: SitesConfigurationModel) -> None: diff --git a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml index 808bdf57..505a3be3 100644 --- a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml +++ b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml @@ -1,5 +1,5 @@ sites_config_enable: true -sites: +sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml index d2f00fbb..1a873760 100644 --- a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml +++ b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml @@ -1,5 +1,5 @@ sites_config_enable: true -sites: +sites_config: items: - domain: example.com name: Example site (revised) diff --git a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml index 808bdf57..505a3be3 100644 --- a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml +++ b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml @@ -1,5 +1,5 @@ sites_config_enable: true -sites: +sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml index e8995718..ac8fa7ca 100644 --- a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml +++ b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml @@ -1,5 +1,5 @@ sites_config_enable: true -sites: +sites_config: items: - domain: example.com name: Example site From 9615bcf1232896ff762a2f6ffef0ad38dd26ec67 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 11:41:35 +0100 Subject: [PATCH 18/32] [#467] remove `Field` call --- src/objects/setup_configuration/models/sites.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/objects/setup_configuration/models/sites.py b/src/objects/setup_configuration/models/sites.py index 347d8e0d..360bdbbc 100644 --- a/src/objects/setup_configuration/models/sites.py +++ b/src/objects/setup_configuration/models/sites.py @@ -1,7 +1,6 @@ from django.contrib.sites.models import Site from django_setup_configuration.models import ConfigurationModel -from pydantic import Field class SiteConfigurationModel(ConfigurationModel): @@ -15,4 +14,4 @@ class Meta: class SitesConfigurationModel(ConfigurationModel): - items: list[SiteConfigurationModel] = Field() + items: list[SiteConfigurationModel] From 9cda2c11afb306485820b7e353d35c19566ad298 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 11:44:32 +0100 Subject: [PATCH 19/32] [#468] Enable TokenAuthConfiguration --- docker/setup_configuration/data.yaml | 11 +++ src/objects/conf/base.py | 1 + .../setup_configuration/models/token_auth.py | 24 +++++++ .../setup_configuration/steps/token_auth.py | 72 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 src/objects/setup_configuration/models/token_auth.py create mode 100644 src/objects/setup_configuration/steps/token_auth.py diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 92eb6326..7b3e1ee4 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -1,3 +1,14 @@ +objects_tokens_config_enable: true +objects_tokens: + items: + - identifier: token-1 + token: 18b2b74ef994314b84021d47b9422e82b685d82f + contact_person: Person 1 + email: person-1@example.com + organization: Organization 1 + application: Application 1 + administration: Administration 1 + zgw_consumers_config_enable: true zgw_consumers: services: diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 201c4f70..8dfcbdb3 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -85,6 +85,7 @@ # SETUP_CONFIGURATION_STEPS = ( "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", + "objects.setup_configuration.steps.token_auth.TokenAuthConfigurationStep", "objects.setup_configuration.steps.sites.SitesConfigurationStep", "objects.setup_configuration.steps.objecttypes.ObjectTypesConfigurationStep", ) diff --git a/src/objects/setup_configuration/models/token_auth.py b/src/objects/setup_configuration/models/token_auth.py new file mode 100644 index 00000000..1d41a17c --- /dev/null +++ b/src/objects/setup_configuration/models/token_auth.py @@ -0,0 +1,24 @@ +from django_setup_configuration.models import ConfigurationModel +from pydantic import Field + +from objects.token.models import TokenAuth + + +class TokenAuthConfigurationModel(ConfigurationModel): + class Meta: + django_model_refs = { + TokenAuth: ( + "identifier", + "token", + "contact_person", + "email", + "organization", + "application", + "administration", + "is_superuser", + ) + } + + +class TokenAuthGroupConfigurationModel(ConfigurationModel): + items: list[TokenAuthConfigurationModel] = Field() diff --git a/src/objects/setup_configuration/steps/token_auth.py b/src/objects/setup_configuration/steps/token_auth.py new file mode 100644 index 00000000..17dd9e4a --- /dev/null +++ b/src/objects/setup_configuration/steps/token_auth.py @@ -0,0 +1,72 @@ +import logging + +from django.core.exceptions import ValidationError +from django.db import IntegrityError + +from django_setup_configuration.configuration import BaseConfigurationStep +from django_setup_configuration.exceptions import ConfigurationRunFailed + +from objects.setup_configuration.models.token_auth import ( + TokenAuthGroupConfigurationModel, +) +from objects.token.models import TokenAuth + +logger = logging.getLogger(__name__) + + +class TokenAuthConfigurationStep( + BaseConfigurationStep[TokenAuthGroupConfigurationModel] +): + """ + Configure tokens for other applications to access Objects API + """ + + namespace = "objects_tokens" + enable_setting = "objects_tokens_config_enable" + + verbose_name = "Configuration to set up authentication tokens for objects" + config_model = TokenAuthGroupConfigurationModel + + def execute(self, model: TokenAuthGroupConfigurationModel) -> None: + for item in model.items: + logger.info(f"Configuring {item.identifier}") + + model_kwargs = { + "identifier": item.identifier, + "token": item.token, + "contact_person": item.contact_person, + "email": item.email, + "organization": item.organization, + "application": item.application, + "administration": item.administration, + "is_superuser": item.is_superuser, + } + + token_instance = TokenAuth(**model_kwargs) + + try: + token_instance.full_clean(exclude=("id",), validate_unique=False) + except ValidationError as exception: + exception_message = ( + f"Validation error(s) occured for {item.identifier}." + ) + raise ConfigurationRunFailed(exception_message) from exception + + logger.debug(f"No validation errors found for {item.identifier}") + + try: + logger.debug(f"Saving {item.identifier}") + + TokenAuth.objects.update_or_create( + identifier=item.identifier, + defaults={ + key: value + for key, value in model_kwargs.items() + if key != "identifier" + }, + ) + except IntegrityError as exception: + exception_message = f"Failed configuring token {item.identifier}." + raise ConfigurationRunFailed(exception_message) from exception + + logger.info(f"Configured {item.identifier}") From 045088e14d266b4806f3c02fd0d9c706c82803d4 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 12:11:15 +0100 Subject: [PATCH 20/32] [#467] update invalid connection test --- .../tests/files/objecttypes_connection_invalid.yaml | 2 +- src/objects/core/tests/test_objecttype_config.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml index c12d0270..6394bed1 100644 --- a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml +++ b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml @@ -13,4 +13,4 @@ objects_api_objecttypes_connection: nlx: http://some-outway-adress.local:8080/ user_id: objects-api user_representation: Objects API - timeout: 60 + timeout: foobar diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py index 706aef9f..e5751f9a 100644 --- a/src/objects/core/tests/test_objecttype_config.py +++ b/src/objects/core/tests/test_objecttype_config.py @@ -2,6 +2,7 @@ from django.test import TestCase +from django_setup_configuration.exceptions import PrerequisiteFailed from django_setup_configuration.test_utils import execute_single_step from zgw_consumers.constants import APITypes, AuthTypes from zgw_consumers.models import Service @@ -80,12 +81,13 @@ def test_update_connection(self): self.assertEqual(service.user_representation, "Objects API") self.assertEqual(service.timeout, 60) - def test_invalid_identifier(self): + def test_invalid_connection(self): test_file_path = str(TEST_FILES / "objecttypes_connection_invalid.yaml") - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) + with self.assertRaises(PrerequisiteFailed): + execute_single_step( + ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path + ) self.assertEqual(Service.objects.count(), 0) From 433867d9b799e54a0a2bf86bbe23b0dcb9e49dd7 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 12:25:29 +0100 Subject: [PATCH 21/32] [#467] Prefix sites configuration namespace with project name --- docs/installation/config_cli.rst | 4 ++-- src/objects/setup_configuration/steps/sites.py | 4 ++-- .../setup_configuration/tests/files/sites_empty_database.yaml | 4 ++-- .../setup_configuration/tests/files/sites_existing_sites.yaml | 4 ++-- .../tests/files/sites_idempotent_step.yaml | 4 ++-- .../setup_configuration/tests/files/sites_invalid_domain.yaml | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 8cf749c1..a16e69b9 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -39,8 +39,8 @@ Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml ... - sites_config_enable: true - sites_config: + objects_api_sites_config_enable: true + objects_api_sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/steps/sites.py b/src/objects/setup_configuration/steps/sites.py index e92c5627..1188226d 100644 --- a/src/objects/setup_configuration/steps/sites.py +++ b/src/objects/setup_configuration/steps/sites.py @@ -12,8 +12,8 @@ class SitesConfigurationStep(BaseConfigurationStep): config_model = SitesConfigurationModel verbose_name = "Sites configuration" - namespace = "sites_config" - enable_setting = "sites_config_enable" + namespace = "objects_api_sites_config" + enable_setting = "objects_api_config_enable" def execute(self, model: SitesConfigurationModel) -> None: for item in model.items: diff --git a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml index 505a3be3..66a9b6c6 100644 --- a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml +++ b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml @@ -1,5 +1,5 @@ -sites_config_enable: true -sites_config: +objects_api_sites_config_enable: true +objects_api_sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml index 1a873760..605ea0d3 100644 --- a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml +++ b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml @@ -1,5 +1,5 @@ -sites_config_enable: true -sites_config: +objects_api_sites_config_enable: true +objects_api_sites_config: items: - domain: example.com name: Example site (revised) diff --git a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml index 505a3be3..66a9b6c6 100644 --- a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml +++ b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml @@ -1,5 +1,5 @@ -sites_config_enable: true -sites_config: +objects_api_sites_config_enable: true +objects_api_sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml index ac8fa7ca..48db8bf7 100644 --- a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml +++ b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml @@ -1,5 +1,5 @@ -sites_config_enable: true -sites_config: +objects_api_sites_config_enable: true +objects_api_sites_config: items: - domain: example.com name: Example site From 95bd575130b246eb9e653d2ca8ebb1a32cfbe990 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 12:39:12 +0100 Subject: [PATCH 22/32] Revert "[#467] Prefix sites configuration namespace with project name" This reverts commit 433867d9b799e54a0a2bf86bbe23b0dcb9e49dd7. --- docs/installation/config_cli.rst | 4 ++-- src/objects/setup_configuration/steps/sites.py | 4 ++-- .../setup_configuration/tests/files/sites_empty_database.yaml | 4 ++-- .../setup_configuration/tests/files/sites_existing_sites.yaml | 4 ++-- .../tests/files/sites_idempotent_step.yaml | 4 ++-- .../setup_configuration/tests/files/sites_invalid_domain.yaml | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index a16e69b9..8cf749c1 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -39,8 +39,8 @@ Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml ... - objects_api_sites_config_enable: true - objects_api_sites_config: + sites_config_enable: true + sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/steps/sites.py b/src/objects/setup_configuration/steps/sites.py index 1188226d..e92c5627 100644 --- a/src/objects/setup_configuration/steps/sites.py +++ b/src/objects/setup_configuration/steps/sites.py @@ -12,8 +12,8 @@ class SitesConfigurationStep(BaseConfigurationStep): config_model = SitesConfigurationModel verbose_name = "Sites configuration" - namespace = "objects_api_sites_config" - enable_setting = "objects_api_config_enable" + namespace = "sites_config" + enable_setting = "sites_config_enable" def execute(self, model: SitesConfigurationModel) -> None: for item in model.items: diff --git a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml index 66a9b6c6..505a3be3 100644 --- a/src/objects/setup_configuration/tests/files/sites_empty_database.yaml +++ b/src/objects/setup_configuration/tests/files/sites_empty_database.yaml @@ -1,5 +1,5 @@ -objects_api_sites_config_enable: true -objects_api_sites_config: +sites_config_enable: true +sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml index 605ea0d3..1a873760 100644 --- a/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml +++ b/src/objects/setup_configuration/tests/files/sites_existing_sites.yaml @@ -1,5 +1,5 @@ -objects_api_sites_config_enable: true -objects_api_sites_config: +sites_config_enable: true +sites_config: items: - domain: example.com name: Example site (revised) diff --git a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml index 66a9b6c6..505a3be3 100644 --- a/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml +++ b/src/objects/setup_configuration/tests/files/sites_idempotent_step.yaml @@ -1,5 +1,5 @@ -objects_api_sites_config_enable: true -objects_api_sites_config: +sites_config_enable: true +sites_config: items: - domain: example.com name: Example site diff --git a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml index 48db8bf7..ac8fa7ca 100644 --- a/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml +++ b/src/objects/setup_configuration/tests/files/sites_invalid_domain.yaml @@ -1,5 +1,5 @@ -objects_api_sites_config_enable: true -objects_api_sites_config: +sites_config_enable: true +sites_config: items: - domain: example.com name: Example site From 7f4fd36423cad73871969647b749b9bfd52929ad Mon Sep 17 00:00:00 2001 From: SonnyBA Date: Wed, 11 Dec 2024 14:05:24 +0100 Subject: [PATCH 23/32] [#467] remove typo in documentation Co-authored-by: Sidney Richards --- docs/installation/config_cli.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index 8cf749c1..c6504f7c 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -58,7 +58,7 @@ Create or update a (single) YAML configuration file with your settings: .. code-block:: yaml objects_api_objecttypes_connection_config_enable: true - objects_api_objecttypes_connection": + objects_api_objecttypes_connection: identifier: objecttypen label: ObjectTypen API api_root: http://objecttypen.nl/api/v1/ From d4b1de16f083d47df7c0495410f43ee86db14486 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 14:36:30 +0100 Subject: [PATCH 24/32] [#467] update docker-compose example configuration --- docker/setup_configuration/data.yaml | 36 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 92eb6326..4691e1e6 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -1,16 +1,22 @@ -zgw_consumers_config_enable: true -zgw_consumers: - services: - # minimum required fields - - identifier: objecttypen-test - label: Objecttypen API test - api_root: http://objecttypen.local/api/v1/ - api_type: orc - auth_type: api_key - -objecttypes_config_enable: true -objecttypes: +sites_config_enable: true +sites_config: items: - - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 - name: Object Type 1 - service_identifier: objecttypen-test + - domain: example.com + name: Example site + +objects_api_objecttypes_connection_config_enable: true +objects_api_objecttypes_connection: + identifier: objecttypes-api + label: Objecttypen API + api_root: https://objecttypes.nl/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: zgw + header_key: Authorization + header_value: Token foo + client_id: client + secret: secret + nlx: http://some-outway-adress.local:8080/ + user_id: objects-api + user_representation: Objects API + timeout: 60 From 4f1767da89a7289217e01c83021b41f8873eced7 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Wed, 11 Dec 2024 15:03:25 +0100 Subject: [PATCH 25/32] [#467] remove unused configuration step --- src/objects/conf/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 09be2714..5ad3e3ca 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -84,7 +84,6 @@ # Django setup configuration # SETUP_CONFIGURATION_STEPS = ( - "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep", "objects.setup_configuration.steps.sites.SitesConfigurationStep", "objects.setup_configuration.steps.objecttypes.ObjectTypesConnectionConfigurationStep", ) From 709b5c1a264a4fa00eb9f8c11ed7347cfe89b154 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 15:04:47 +0100 Subject: [PATCH 26/32] [#468] Change namespaces for TokenAuthConfiguration --- docker/setup_configuration/data.yaml | 5 +++-- src/objects/setup_configuration/steps/token_auth.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 7b3e1ee4..4a4a4c43 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -1,5 +1,5 @@ -objects_tokens_config_enable: true -objects_tokens: +token_tokenauth_config_enable: true +token_tokenauth: items: - identifier: token-1 token: 18b2b74ef994314b84021d47b9422e82b685d82f @@ -25,3 +25,4 @@ objecttypes: - uuid: b427ef84-189d-43aa-9efd-7bb2c459e281 name: Object Type 1 service_identifier: objecttypen-test + diff --git a/src/objects/setup_configuration/steps/token_auth.py b/src/objects/setup_configuration/steps/token_auth.py index 17dd9e4a..817acf3b 100644 --- a/src/objects/setup_configuration/steps/token_auth.py +++ b/src/objects/setup_configuration/steps/token_auth.py @@ -21,8 +21,8 @@ class TokenAuthConfigurationStep( Configure tokens for other applications to access Objects API """ - namespace = "objects_tokens" - enable_setting = "objects_tokens_config_enable" + namespace = "token_tokenauth" + enable_setting = "token_tokenauth_config_enable" verbose_name = "Configuration to set up authentication tokens for objects" config_model = TokenAuthGroupConfigurationModel From e871559d6d7eb647b78087247e483b7197e33fb4 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 15:27:08 +0100 Subject: [PATCH 27/32] [#468] Add tests TokenAuthConfiguration --- .../models}/__init__.py | 0 .../setup_configuration/steps/__init__.py | 0 .../tests/files/token_auth_invalid_setup.yaml | 3 + .../token_auth_valid_setup_complete.yaml | 18 + .../files/token_auth_valid_setup_default.yaml | 12 + .../tests/test_token_auth_config.py | 365 ++++++++++++++++++ src/objects/token/tests/factories/__init__.py | 0 src/objects/token/tests/factories/token.py | 12 + 8 files changed, 410 insertions(+) rename src/objects/{tests/config => setup_configuration/models}/__init__.py (100%) create mode 100644 src/objects/setup_configuration/steps/__init__.py create mode 100644 src/objects/setup_configuration/tests/files/token_auth_invalid_setup.yaml create mode 100644 src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml create mode 100644 src/objects/setup_configuration/tests/files/token_auth_valid_setup_default.yaml create mode 100644 src/objects/setup_configuration/tests/test_token_auth_config.py create mode 100644 src/objects/token/tests/factories/__init__.py create mode 100644 src/objects/token/tests/factories/token.py diff --git a/src/objects/tests/config/__init__.py b/src/objects/setup_configuration/models/__init__.py similarity index 100% rename from src/objects/tests/config/__init__.py rename to src/objects/setup_configuration/models/__init__.py diff --git a/src/objects/setup_configuration/steps/__init__.py b/src/objects/setup_configuration/steps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objects/setup_configuration/tests/files/token_auth_invalid_setup.yaml b/src/objects/setup_configuration/tests/files/token_auth_invalid_setup.yaml new file mode 100644 index 00000000..2749f17a --- /dev/null +++ b/src/objects/setup_configuration/tests/files/token_auth_invalid_setup.yaml @@ -0,0 +1,3 @@ +token_tokenauth_config_enable: true +token_tokenauth: + items: \ No newline at end of file diff --git a/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml new file mode 100644 index 00000000..aca6cca3 --- /dev/null +++ b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml @@ -0,0 +1,18 @@ +token_tokenauth_config_enable: true +token_tokenauth: + items: + - identifier: token-1 + token: 18b2b74ef994314b84021d47b9422e82b685d82f + contact_person: Person 1 + email: person-1@example.com + organization: Organization 1 + application: Application 1 + administration: Administration 1 + + - identifier: token-2 + token: e882642bd0ec2482adcdc97258c2e6f98cb06d85 + contact_person: Person 2 + email: person-2@example.com + organization: Organization 2 + application: Application 2 + administration: Administration 2 diff --git a/src/objects/setup_configuration/tests/files/token_auth_valid_setup_default.yaml b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_default.yaml new file mode 100644 index 00000000..b9af9475 --- /dev/null +++ b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_default.yaml @@ -0,0 +1,12 @@ +token_tokenauth_config_enable: true +token_tokenauth: + items: + - identifier: token-1 + token: 18b2b74ef994314b84021d47b9422e82b685d82f + contact_person: Person 1 + email: person-1@example.com + + - identifier: token-2 + token: e882642bd0ec2482adcdc97258c2e6f98cb06d85 + contact_person: Person 2 + email: person-2@example.com diff --git a/src/objects/setup_configuration/tests/test_token_auth_config.py b/src/objects/setup_configuration/tests/test_token_auth_config.py new file mode 100644 index 00000000..17378c00 --- /dev/null +++ b/src/objects/setup_configuration/tests/test_token_auth_config.py @@ -0,0 +1,365 @@ +from pathlib import Path + +from django.test import TestCase + +from django_setup_configuration.exceptions import ( + ConfigurationRunFailed, + PrerequisiteFailed, +) +from django_setup_configuration.test_utils import execute_single_step + +from objects.setup_configuration.steps.token_auth import TokenAuthConfigurationStep +from objects.token.models import TokenAuth +from objects.token.tests.factories.token import TokenAuthFactory + +DIR_FILES = (Path(__file__).parent / "files").resolve() + + +class TokenAuthConfigurationStepTests(TestCase): + def test_valid_setup_default(self): + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_valid_setup_default.yaml"), + ) + + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + token = tokens.get(identifier="token-1") + self.assertEqual(token.token, "18b2b74ef994314b84021d47b9422e82b685d82f") + self.assertEqual(token.contact_person, "Person 1") + self.assertEqual(token.email, "person-1@example.com") + self.assertEqual(token.organization, "") + self.assertEqual(token.application, "") + self.assertEqual(token.administration, "") + + token = tokens.get(identifier="token-2") + self.assertEqual(token.contact_person, "Person 2") + self.assertEqual(token.token, "e882642bd0ec2482adcdc97258c2e6f98cb06d85") + self.assertEqual(token.email, "person-2@example.com") + self.assertEqual(token.organization, "") + self.assertEqual(token.application, "") + self.assertEqual(token.administration, "") + + def test_valid_setup_complete(self): + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_valid_setup_complete.yaml"), + ) + + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + # Same as configuration + token = tokens.get(identifier="token-1") + self.assertEqual(token.token, "18b2b74ef994314b84021d47b9422e82b685d82f") + self.assertEqual(token.contact_person, "Person 1") + self.assertEqual(token.email, "person-1@example.com") + self.assertEqual(token.organization, "Organization 1") + self.assertEqual(token.application, "Application 1") + self.assertEqual(token.administration, "Administration 1") + + # Token data updated + token = tokens.get(identifier="token-2") + self.assertEqual(token.contact_person, "Person 2") + self.assertEqual(token.token, "e882642bd0ec2482adcdc97258c2e6f98cb06d85") + self.assertEqual(token.email, "person-2@example.com") + self.assertEqual(token.organization, "Organization 2") + self.assertEqual(token.application, "Application 2") + self.assertEqual(token.administration, "Administration 2") + + self.assertNotEqual(token.token, "1cad42916dfa439af8c69000bf7b6af6a66782af") + self.assertNotEqual(token.contact_person, "Person 3") + self.assertNotEqual(token.email, "person-3@example.com") + + def test_valid_update_existing_tokens(self): + TokenAuthFactory( + identifier="token-1", + token="18b2b74ef994314b84021d47b9422e82b685d82f", + contact_person="Person 1", + email="person-1@example.com", + organization="Organization XYZ", + application="Application XYZ", + administration="Administration XYZ", + ) + + TokenAuthFactory( + identifier="token-2", + token="1cad42916dfa439af8c69000bf7b6af6a66782af", + contact_person="Person 3", + email="person-3@example.com", + ) + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_valid_setup_complete.yaml"), + ) + + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + # Same as configuration + token = tokens.get(identifier="token-1") + self.assertEqual(token.token, "18b2b74ef994314b84021d47b9422e82b685d82f") + self.assertEqual(token.contact_person, "Person 1") + self.assertEqual(token.email, "person-1@example.com") + self.assertEqual(token.organization, "Organization 1") + self.assertEqual(token.application, "Application 1") + self.assertEqual(token.administration, "Administration 1") + + # Token data updated + token = tokens.get(identifier="token-2") + self.assertEqual(token.contact_person, "Person 2") + self.assertEqual(token.token, "e882642bd0ec2482adcdc97258c2e6f98cb06d85") + self.assertEqual(token.email, "person-2@example.com") + self.assertEqual(token.organization, "Organization 2") + self.assertEqual(token.application, "Application 2") + self.assertEqual(token.administration, "Administration 2") + + self.assertNotEqual(token.token, "1cad42916dfa439af8c69000bf7b6af6a66782af") + self.assertNotEqual(token.contact_person, "Person 3") + self.assertNotEqual(token.email, "person-3@example.com") + + def test_valid_idempotent_step(self): + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_valid_setup_complete.yaml"), + ) + + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + old_token_a = tokens.get(identifier="token-1") + self.assertEqual(old_token_a.identifier, "token-1") + self.assertEqual(old_token_a.token, "18b2b74ef994314b84021d47b9422e82b685d82f") + self.assertEqual(old_token_a.contact_person, "Person 1") + self.assertEqual(old_token_a.email, "person-1@example.com") + self.assertEqual(old_token_a.organization, "Organization 1") + self.assertEqual(old_token_a.application, "Application 1") + self.assertEqual(old_token_a.administration, "Administration 1") + + old_token_b = tokens.get(identifier="token-2") + self.assertEqual(old_token_b.identifier, "token-2") + self.assertEqual(old_token_b.contact_person, "Person 2") + self.assertEqual(old_token_b.token, "e882642bd0ec2482adcdc97258c2e6f98cb06d85") + self.assertEqual(old_token_b.email, "person-2@example.com") + self.assertEqual(old_token_b.organization, "Organization 2") + self.assertEqual(old_token_b.application, "Application 2") + self.assertEqual(old_token_b.administration, "Administration 2") + + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_valid_setup_complete.yaml"), + ) + + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + new_token_a = tokens.get(identifier="token-1") + self.assertEqual(new_token_a.identifier, old_token_a.identifier) + self.assertEqual(new_token_a.token, old_token_a.token) + self.assertEqual(new_token_a.contact_person, old_token_a.contact_person) + self.assertEqual(new_token_a.email, old_token_a.email) + self.assertEqual(new_token_a.organization, old_token_a.organization) + self.assertEqual(new_token_a.application, old_token_a.application) + self.assertEqual(new_token_a.administration, old_token_a.administration) + + new_token_b = tokens.get(identifier="token-2") + self.assertEqual(new_token_b.identifier, old_token_b.identifier) + self.assertEqual(new_token_b.contact_person, old_token_b.contact_person) + self.assertEqual(new_token_b.token, old_token_b.token) + self.assertEqual(new_token_b.email, old_token_b.email) + self.assertEqual(new_token_b.organization, old_token_b.organization) + self.assertEqual(new_token_b.application, old_token_b.application) + self.assertEqual(new_token_b.administration, old_token_b.administration) + + def test_invalid_setup(self): + with self.assertRaises(PrerequisiteFailed) as command_error: + execute_single_step( + TokenAuthConfigurationStep, + yaml_source=str(DIR_FILES / "token_auth_invalid_setup.yaml"), + ) + + self.assertTrue("Input should be a valid list" in str(command_error.exception)) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_setup_email(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067", + "contact_person": "Person 1", + "email": "invalid", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Validation error(s) occured for token-1" in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_setup_token(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "token": "invalid token", + "contact_person": "Person 1", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Validation error(s) occured for token-1" in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_empty_token(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "token": "", + "contact_person": "Person 1", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Validation error(s) occured for token-1" in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_setup_token_missing(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "contact_person": "Person 1", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(PrerequisiteFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue("Field required" in str(command_error.exception)) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_setup_token_unique(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "contact_person": "Person 1", + "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + { + "identifier": "token-2", + "contact_person": "Person 2", + "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067", + "email": "person-2@example.com", + "organization": "Organization 2", + "application": "Application 2", + "administration": "Administration 2", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Failed configuring token token-2" in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 1) + + def test_invalid_setup_contact_person(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "token-1", + "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067", + "contact_person": "", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Validation error(s) occured for token-1" in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 0) + + def test_invalid_setup_identifier(self): + object_source = { + "token_tokenauth_config_enable": True, + "token_tokenauth": { + "items": [ + { + "identifier": "invalid identifier", + "token": "ba9d233e95e04c4a8a661a27daffe7c9bd019067", + "contact_person": "Person 1", + "email": "person-1@example.com", + "organization": "Organization 1", + "application": "Application 1", + "administration": "Administration 1", + }, + ], + }, + } + with self.assertRaises(ConfigurationRunFailed) as command_error: + execute_single_step(TokenAuthConfigurationStep, object_source=object_source) + + self.assertTrue( + "Validation error(s) occured for invalid identifier" + in str(command_error.exception) + ) + self.assertEqual(TokenAuth.objects.count(), 0) diff --git a/src/objects/token/tests/factories/__init__.py b/src/objects/token/tests/factories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/objects/token/tests/factories/token.py b/src/objects/token/tests/factories/token.py new file mode 100644 index 00000000..66193305 --- /dev/null +++ b/src/objects/token/tests/factories/token.py @@ -0,0 +1,12 @@ +import factory + +from objects.token.models import TokenAuth + + +class TokenAuthFactory(factory.django.DjangoModelFactory): + identifier = factory.Sequence(lambda sequence: f"token-{sequence}") + contact_person = factory.Faker("name") + email = factory.Faker("email") + + class Meta: + model = TokenAuth From 5536da3a190ece438142492d10d6f700210cc78d Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 15:31:48 +0100 Subject: [PATCH 28/32] [#468] Add migrations test --- .../tests/test_token_auth_config.py | 2 +- src/objects/token/models.py | 3 +- src/objects/token/tests/factories.py | 1 + src/objects/token/tests/factories/__init__.py | 0 src/objects/token/tests/factories/token.py | 12 --- src/objects/token/tests/test_migrations.py | 88 +++++++++++++++++++ 6 files changed, 91 insertions(+), 15 deletions(-) delete mode 100644 src/objects/token/tests/factories/__init__.py delete mode 100644 src/objects/token/tests/factories/token.py create mode 100644 src/objects/token/tests/test_migrations.py diff --git a/src/objects/setup_configuration/tests/test_token_auth_config.py b/src/objects/setup_configuration/tests/test_token_auth_config.py index 17378c00..737c28d6 100644 --- a/src/objects/setup_configuration/tests/test_token_auth_config.py +++ b/src/objects/setup_configuration/tests/test_token_auth_config.py @@ -10,7 +10,7 @@ from objects.setup_configuration.steps.token_auth import TokenAuthConfigurationStep from objects.token.models import TokenAuth -from objects.token.tests.factories.token import TokenAuthFactory +from objects.token.tests.factories import TokenAuthFactory DIR_FILES = (Path(__file__).parent / "files").resolve() diff --git a/src/objects/token/models.py b/src/objects/token/models.py index 5731d590..83b9f0c8 100644 --- a/src/objects/token/models.py +++ b/src/objects/token/models.py @@ -6,11 +6,10 @@ from django.utils.translation import gettext_lazy as _ from objects.core.models import ObjectType +from objects.token.validators import validate_whitespace from .constants import PermissionModes -from objects.token.validators import validate_whitespace - class TokenAuth(models.Model): identifier = models.SlugField( diff --git a/src/objects/token/tests/factories.py b/src/objects/token/tests/factories.py index 35246763..0baf0d32 100644 --- a/src/objects/token/tests/factories.py +++ b/src/objects/token/tests/factories.py @@ -7,6 +7,7 @@ class TokenAuthFactory(factory.django.DjangoModelFactory): + identifier = factory.Sequence(lambda sequence: f"token-{sequence}") contact_person = factory.Faker("name") email = factory.Faker("email") diff --git a/src/objects/token/tests/factories/__init__.py b/src/objects/token/tests/factories/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/objects/token/tests/factories/token.py b/src/objects/token/tests/factories/token.py deleted file mode 100644 index 66193305..00000000 --- a/src/objects/token/tests/factories/token.py +++ /dev/null @@ -1,12 +0,0 @@ -import factory - -from objects.token.models import TokenAuth - - -class TokenAuthFactory(factory.django.DjangoModelFactory): - identifier = factory.Sequence(lambda sequence: f"token-{sequence}") - contact_person = factory.Faker("name") - email = factory.Faker("email") - - class Meta: - model = TokenAuth diff --git a/src/objects/token/tests/test_migrations.py b/src/objects/token/tests/test_migrations.py new file mode 100644 index 00000000..f4834931 --- /dev/null +++ b/src/objects/token/tests/test_migrations.py @@ -0,0 +1,88 @@ +from django.core.management import call_command +from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.db.migrations.state import StateApps +from django.test import TransactionTestCase + + +class BaseMigrationTest(TransactionTestCase): + app: str + migrate_from: str # The migration before the one we want to test + migrate_to: str # The migration we want to test + + setting_overrides: dict = {} + + old_app_state: StateApps + app_state: StateApps + + def setUp(self) -> None: + """ + Setup the migration test by reversing to `migrate_from` state, + then applying the `migrate_to` state. + """ + assert self.app is not None, "You must define the `app` attribute" + assert self.migrate_from is not None, "You must define `migrate_from`" + assert self.migrate_to is not None, "You must define `migrate_to`" + + # Step 1: Set up the MigrationExecutor + executor = MigrationExecutor(connection) + + # Step 2: Reverse to the starting migration state + migrate_from = [(self.app, self.migrate_from)] + old_migrate_state = executor.migrate(migrate_from) + + self.old_app_state = old_migrate_state.apps + + def _perform_migration(self) -> None: + migrate_to = [(self.app, self.migrate_to)] + + executor = MigrationExecutor(connection) + executor.loader.build_graph() # reload the graph in case of dependency changes + executor.migrate(migrate_to) + + self.apps = executor.loader.project_state(migrate_to).apps + + @classmethod + def tearDownClass(cls) -> None: + super().tearDownClass() + + # reset to latest migration + call_command("migrate", verbosity=0, database=connection._alias) + + +class TestTokenAuthUniqueness(BaseMigrationTest): + app = "token" + migrate_from = "0016_alter_permission_token_auth" + migrate_to = "0017_tokenauth_identifier_alter_tokenauth_token" + + def test_migrate_tokens_check_attr(self): + TokenAuth = self.old_app_state.get_model("token", "TokenAuth") + self.assertFalse(hasattr(TokenAuth, "identifier")) + + self._perform_migration() + + TokenAuth = self.apps.get_model("token", "TokenAuth") + self.assertTrue(hasattr(TokenAuth, "identifier")) + + def test_migrate_tokens_to_unique_identifiers(self): + TokenAuth = self.old_app_state.get_model("token", "TokenAuth") + TokenAuth.objects.create( + token="aa018d1c576c9dae33be1e549f739f2834ebc811", + contact_person="Person 1", + email="test@example.com", + ) + TokenAuth.objects.create( + token="ab700d6bf906c2b4b42a961c529657314c6a8246", + contact_person="Other person", + email="somebody@else.com", + ) + + self._perform_migration() + + TokenAuth = self.apps.get_model("token", "TokenAuth") + tokens = TokenAuth.objects.all() + self.assertEqual(tokens.count(), 2) + + first_token = tokens.get(token="aa018d1c576c9dae33be1e549f739f2834ebc811") + second_token = tokens.get(token="ab700d6bf906c2b4b42a961c529657314c6a8246") + self.assertNotEqual(first_token.identifier, second_token.identifier) From 971c775609d054093c09678aae92c3c31d1e98e3 Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 15:53:57 +0100 Subject: [PATCH 29/32] [#468] Update config_cli.rst --- docs/installation/config_cli.rst | 33 +++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index c6504f7c..e2a4d490 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -26,8 +26,7 @@ Preparation The command executes the list of pluggable configuration steps, and each step requires specific configuration information, that should be prepared. Here is the description of all available configuration steps and the configuration -format, use by each step. - +format, used by each step. Objects API =========== @@ -69,14 +68,38 @@ Create or update a (single) YAML configuration file with your settings: header_value: Token foo ... +TokenAuth configuration +------------------------- + +Create or update a (single) YAML configuration file with your settings: + +.. code-block:: yaml + token_tokenauth_config_enable: true + token_tokenauth: + items: + - identifier: token-1 + token: 18b2b74ef994314b84021d47b9422e82b685d82f + contact_person: Person 1 + email: person-1@example.com + organization: Organization XYZ + application: Application XYZ + administration: Administration XYZ + + - identifier: token-2 + token: e882642bd0ec2482adcdc97258c2e6f98cb06d85 + contact_person: Person 2 + email: person-2@example.com + ... + Execution ========= -With the full command invocation, everything is configured at once and immediately -tested. +With the full command invocation, everything is configured at once. +Each configuration step is idempotent, so any manual changes made via the admin interface +will be updated if the command is run afterwards. .. code-block:: bash - src/manage.py setup_configuration --yaml-file /path/to/config.yaml + python ./src/manage.py setup_configuration --yaml-file /path/to/config.yaml From d7a2dd7c805195484b32c437d4837aa37a08a83c Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Wed, 11 Dec 2024 16:53:12 +0100 Subject: [PATCH 30/32] [#468] Improvements --- requirements/base.in | 1 + src/objects/setup_configuration/models/token_auth.py | 2 +- .../tests/files/token_auth_valid_setup_complete.yaml | 2 ++ .../setup_configuration/tests/test_token_auth_config.py | 8 ++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/requirements/base.in b/requirements/base.in index 8621c029..ba97884f 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -7,4 +7,5 @@ furl # Common ground libraries notifications-api-common +django-setup-configuration>=0.4.0 zgw-consumers[setup-configuration] diff --git a/src/objects/setup_configuration/models/token_auth.py b/src/objects/setup_configuration/models/token_auth.py index 1d41a17c..f85d1730 100644 --- a/src/objects/setup_configuration/models/token_auth.py +++ b/src/objects/setup_configuration/models/token_auth.py @@ -21,4 +21,4 @@ class Meta: class TokenAuthGroupConfigurationModel(ConfigurationModel): - items: list[TokenAuthConfigurationModel] = Field() + items: list[TokenAuthConfigurationModel] diff --git a/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml index aca6cca3..5f91577c 100644 --- a/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml +++ b/src/objects/setup_configuration/tests/files/token_auth_valid_setup_complete.yaml @@ -8,6 +8,7 @@ token_tokenauth: organization: Organization 1 application: Application 1 administration: Administration 1 + is_superuser: True - identifier: token-2 token: e882642bd0ec2482adcdc97258c2e6f98cb06d85 @@ -16,3 +17,4 @@ token_tokenauth: organization: Organization 2 application: Application 2 administration: Administration 2 + is_superuser: True diff --git a/src/objects/setup_configuration/tests/test_token_auth_config.py b/src/objects/setup_configuration/tests/test_token_auth_config.py index 737c28d6..3986b066 100644 --- a/src/objects/setup_configuration/tests/test_token_auth_config.py +++ b/src/objects/setup_configuration/tests/test_token_auth_config.py @@ -32,6 +32,7 @@ def test_valid_setup_default(self): self.assertEqual(token.organization, "") self.assertEqual(token.application, "") self.assertEqual(token.administration, "") + self.assertFalse(token.is_superuser) token = tokens.get(identifier="token-2") self.assertEqual(token.contact_person, "Person 2") @@ -40,6 +41,7 @@ def test_valid_setup_default(self): self.assertEqual(token.organization, "") self.assertEqual(token.application, "") self.assertEqual(token.administration, "") + self.assertFalse(token.is_superuser) def test_valid_setup_complete(self): execute_single_step( @@ -58,6 +60,7 @@ def test_valid_setup_complete(self): self.assertEqual(token.organization, "Organization 1") self.assertEqual(token.application, "Application 1") self.assertEqual(token.administration, "Administration 1") + self.assertTrue(token.is_superuser) # Token data updated token = tokens.get(identifier="token-2") @@ -67,6 +70,7 @@ def test_valid_setup_complete(self): self.assertEqual(token.organization, "Organization 2") self.assertEqual(token.application, "Application 2") self.assertEqual(token.administration, "Administration 2") + self.assertTrue(token.is_superuser) self.assertNotEqual(token.token, "1cad42916dfa439af8c69000bf7b6af6a66782af") self.assertNotEqual(token.contact_person, "Person 3") @@ -105,6 +109,7 @@ def test_valid_update_existing_tokens(self): self.assertEqual(token.organization, "Organization 1") self.assertEqual(token.application, "Application 1") self.assertEqual(token.administration, "Administration 1") + self.assertTrue(token.is_superuser) # Token data updated token = tokens.get(identifier="token-2") @@ -114,6 +119,7 @@ def test_valid_update_existing_tokens(self): self.assertEqual(token.organization, "Organization 2") self.assertEqual(token.application, "Application 2") self.assertEqual(token.administration, "Administration 2") + self.assertTrue(token.is_superuser) self.assertNotEqual(token.token, "1cad42916dfa439af8c69000bf7b6af6a66782af") self.assertNotEqual(token.contact_person, "Person 3") @@ -136,6 +142,7 @@ def test_valid_idempotent_step(self): self.assertEqual(old_token_a.organization, "Organization 1") self.assertEqual(old_token_a.application, "Application 1") self.assertEqual(old_token_a.administration, "Administration 1") + self.assertTrue(old_token_a.is_superuser) old_token_b = tokens.get(identifier="token-2") self.assertEqual(old_token_b.identifier, "token-2") @@ -145,6 +152,7 @@ def test_valid_idempotent_step(self): self.assertEqual(old_token_b.organization, "Organization 2") self.assertEqual(old_token_b.application, "Application 2") self.assertEqual(old_token_b.administration, "Administration 2") + self.assertTrue(old_token_b.is_superuser) execute_single_step( TokenAuthConfigurationStep, From 3903e57715a533b4d9dea135de112db2739dc7bc Mon Sep 17 00:00:00 2001 From: Daniel Mursa Date: Thu, 12 Dec 2024 10:21:36 +0100 Subject: [PATCH 31/32] [#468] Fix tests --- .../setup_configuration/tests/test_token_auth_config.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/objects/setup_configuration/tests/test_token_auth_config.py b/src/objects/setup_configuration/tests/test_token_auth_config.py index 3986b066..ba94d732 100644 --- a/src/objects/setup_configuration/tests/test_token_auth_config.py +++ b/src/objects/setup_configuration/tests/test_token_auth_config.py @@ -72,10 +72,6 @@ def test_valid_setup_complete(self): self.assertEqual(token.administration, "Administration 2") self.assertTrue(token.is_superuser) - self.assertNotEqual(token.token, "1cad42916dfa439af8c69000bf7b6af6a66782af") - self.assertNotEqual(token.contact_person, "Person 3") - self.assertNotEqual(token.email, "person-3@example.com") - def test_valid_update_existing_tokens(self): TokenAuthFactory( identifier="token-1", From 23f5756b584c0ca44503447d760b480e0912346a Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Thu, 12 Dec 2024 11:35:02 +0100 Subject: [PATCH 32/32] [#467] use zgw-consumers step to configure objecttypes connection --- docker/setup_configuration/data.yaml | 25 ++-- docs/installation/config_cli.rst | 29 ++-- src/objects/conf/base.py | 2 +- .../tests/files/objecttypes_connection.yaml | 16 -- .../files/objecttypes_connection_invalid.yaml | 16 -- .../core/tests/test_objecttype_config.py | 137 ------------------ .../setup_configuration/steps/objecttypes.py | 51 ------- 7 files changed, 28 insertions(+), 248 deletions(-) delete mode 100644 src/objects/core/tests/files/objecttypes_connection.yaml delete mode 100644 src/objects/core/tests/files/objecttypes_connection_invalid.yaml delete mode 100644 src/objects/core/tests/test_objecttype_config.py delete mode 100644 src/objects/setup_configuration/steps/objecttypes.py diff --git a/docker/setup_configuration/data.yaml b/docker/setup_configuration/data.yaml index 4691e1e6..3853ff5f 100644 --- a/docker/setup_configuration/data.yaml +++ b/docker/setup_configuration/data.yaml @@ -4,19 +4,12 @@ sites_config: - domain: example.com name: Example site -objects_api_objecttypes_connection_config_enable: true -objects_api_objecttypes_connection: - identifier: objecttypes-api - label: Objecttypen API - api_root: https://objecttypes.nl/api/v1/ - api_connection_check_path: objecttypes - api_type: orc - auth_type: zgw - header_key: Authorization - header_value: Token foo - client_id: client - secret: secret - nlx: http://some-outway-adress.local:8080/ - user_id: objects-api - user_representation: Objects API - timeout: 60 +zgw_consumers_config_enable: true +zgw_consumers: + services: + - identifier: objecttypes-api + label: Objecttypes API + api_root: http://objecttypes.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key diff --git a/docs/installation/config_cli.rst b/docs/installation/config_cli.rst index c6504f7c..46d76a15 100644 --- a/docs/installation/config_cli.rst +++ b/docs/installation/config_cli.rst @@ -54,21 +54,28 @@ Create or update a (single) YAML configuration file with your settings: Objecttypes connection configuration ------------------------- -Create or update a (single) YAML configuration file with your settings: +In order to be able to retrieve objecttypes, a corresponding ``Service`` should be +created. An example of a configuration could be seen below: .. code-block:: yaml - objects_api_objecttypes_connection_config_enable: true - objects_api_objecttypes_connection: - identifier: objecttypen - label: ObjectTypen API - api_root: http://objecttypen.nl/api/v1/ - api_connection_check_path: objecttypes - api_type: orc - auth_type: api_key - header_key: Authorization - header_value: Token foo ... + zgw_consumers_config_enable: true + zgw_consumers: + services: + - identifier: objecttypes-api-1 + label: Objecttypes API 1 + api_root: http://objecttypes-1.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + - identifier: objecttypes-api-2 + label: Objecttypes API 2 + api_root: http://objecttypes-2.local/api/v1/ + api_connection_check_path: objecttypes + api_type: orc + auth_type: api_key + .... Execution ========= diff --git a/src/objects/conf/base.py b/src/objects/conf/base.py index 5ad3e3ca..b9bfa55a 100644 --- a/src/objects/conf/base.py +++ b/src/objects/conf/base.py @@ -84,6 +84,6 @@ # Django setup configuration # SETUP_CONFIGURATION_STEPS = ( + "zgw_consumers.contrib.setup_configuration.steps.ServiceConfigurationStep" "objects.setup_configuration.steps.sites.SitesConfigurationStep", - "objects.setup_configuration.steps.objecttypes.ObjectTypesConnectionConfigurationStep", ) diff --git a/src/objects/core/tests/files/objecttypes_connection.yaml b/src/objects/core/tests/files/objecttypes_connection.yaml deleted file mode 100644 index 0cf3177f..00000000 --- a/src/objects/core/tests/files/objecttypes_connection.yaml +++ /dev/null @@ -1,16 +0,0 @@ -objects_api_objecttypes_connection_config_enable: true -objects_api_objecttypes_connection: - identifier: objecttypes-api - label: Objecttypen API - api_root: https://objecttypes.nl/api/v1/ - api_connection_check_path: objecttypes - api_type: orc - auth_type: zgw - header_key: Authorization - header_value: Token foo - client_id: client - secret: secret - nlx: http://some-outway-adress.local:8080/ - user_id: objects-api - user_representation: Objects API - timeout: 60 diff --git a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml b/src/objects/core/tests/files/objecttypes_connection_invalid.yaml deleted file mode 100644 index 6394bed1..00000000 --- a/src/objects/core/tests/files/objecttypes_connection_invalid.yaml +++ /dev/null @@ -1,16 +0,0 @@ -objects_api_objecttypes_connection_config_enable: true -objects_api_objecttypes_connection: - identifier: identifier with whitespace in between - label: Objecttypen API - api_root: https://objecttypes.nl/api/v1/ - api_connection_check_path: objectttypes - api_type: orc - auth_type: zgw - header_key: Authorization - header_value: Token foo - client_id: client - secret: super-secret - nlx: http://some-outway-adress.local:8080/ - user_id: objects-api - user_representation: Objects API - timeout: foobar diff --git a/src/objects/core/tests/test_objecttype_config.py b/src/objects/core/tests/test_objecttype_config.py deleted file mode 100644 index e5751f9a..00000000 --- a/src/objects/core/tests/test_objecttype_config.py +++ /dev/null @@ -1,137 +0,0 @@ -from pathlib import Path - -from django.test import TestCase - -from django_setup_configuration.exceptions import PrerequisiteFailed -from django_setup_configuration.test_utils import execute_single_step -from zgw_consumers.constants import APITypes, AuthTypes -from zgw_consumers.models import Service -from zgw_consumers.test.factories import ServiceFactory - -from objects.setup_configuration.steps.objecttypes import ( - ObjectTypesConnectionConfigurationStep, -) - -TEST_FILES = (Path(__file__).parent / "files").resolve() - - -class ObjectTypesConnectionConfigurationStepTests(TestCase): - def test_create_connection(self): - test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") - - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) - - service = Service.objects.get(slug="objecttypes-api") - - self.assertEqual(service.label, "Objecttypen API") - self.assertEqual(service.api_type, APITypes.orc) - self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") - self.assertEqual(service.api_connection_check_path, "objecttypes") - self.assertEqual(service.auth_type, AuthTypes.zgw) - self.assertEqual(service.client_id, "client") - self.assertEqual(service.secret, "secret") - self.assertEqual(service.header_key, "Authorization") - self.assertEqual(service.header_value, "Token foo") - self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") - self.assertEqual(service.user_id, "objects-api") - self.assertEqual(service.user_representation, "Objects API") - self.assertEqual(service.timeout, 60) - - def test_update_connection(self): - test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") - - service: Service = ServiceFactory( - slug="objecttypes-api", - label="Objecttypen API Test", - api_type=APITypes.zrc, - api_root="https://test.objecttypes.nl/api/v1/", - api_connection_check_path="objecttype", - auth_type=AuthTypes.api_key, - client_id="test-client", - secret="test-secret", - header_key="authorization", - header_value="Token foobar", - nlx="http://test.some-outway-adress.local:8080/", - user_id="test-objects-api", - user_representation="Test Objects API", - timeout=30, - ) - - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) - - self.assertEqual(Service.objects.count(), 1) - - service.refresh_from_db() - - self.assertEqual(service.label, "Objecttypen API") - self.assertEqual(service.api_type, APITypes.orc) - self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") - self.assertEqual(service.api_connection_check_path, "objecttypes") - self.assertEqual(service.auth_type, AuthTypes.zgw) - self.assertEqual(service.client_id, "client") - self.assertEqual(service.secret, "secret") - self.assertEqual(service.header_key, "Authorization") - self.assertEqual(service.header_value, "Token foo") - self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") - self.assertEqual(service.user_id, "objects-api") - self.assertEqual(service.user_representation, "Objects API") - self.assertEqual(service.timeout, 60) - - def test_invalid_connection(self): - test_file_path = str(TEST_FILES / "objecttypes_connection_invalid.yaml") - - with self.assertRaises(PrerequisiteFailed): - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) - - self.assertEqual(Service.objects.count(), 0) - - def test_idempotent_step(self): - test_file_path = str(TEST_FILES / "objecttypes_connection.yaml") - - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) - - service = Service.objects.get(slug="objecttypes-api") - - self.assertEqual(service.label, "Objecttypen API") - self.assertEqual(service.api_type, APITypes.orc) - self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") - self.assertEqual(service.api_connection_check_path, "objecttypes") - self.assertEqual(service.auth_type, AuthTypes.zgw) - self.assertEqual(service.client_id, "client") - self.assertEqual(service.secret, "secret") - self.assertEqual(service.header_key, "Authorization") - self.assertEqual(service.header_value, "Token foo") - self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") - self.assertEqual(service.user_id, "objects-api") - self.assertEqual(service.user_representation, "Objects API") - self.assertEqual(service.timeout, 60) - - execute_single_step( - ObjectTypesConnectionConfigurationStep, yaml_source=test_file_path - ) - - self.assertEqual(Service.objects.count(), 1) - - service.refresh_from_db() - - self.assertEqual(service.label, "Objecttypen API") - self.assertEqual(service.api_type, APITypes.orc) - self.assertEqual(service.api_root, "https://objecttypes.nl/api/v1/") - self.assertEqual(service.api_connection_check_path, "objecttypes") - self.assertEqual(service.auth_type, AuthTypes.zgw) - self.assertEqual(service.client_id, "client") - self.assertEqual(service.secret, "secret") - self.assertEqual(service.header_key, "Authorization") - self.assertEqual(service.header_value, "Token foo") - self.assertEqual(service.nlx, "http://some-outway-adress.local:8080/") - self.assertEqual(service.user_id, "objects-api") - self.assertEqual(service.user_representation, "Objects API") - self.assertEqual(service.timeout, 60) diff --git a/src/objects/setup_configuration/steps/objecttypes.py b/src/objects/setup_configuration/steps/objecttypes.py deleted file mode 100644 index e99f3061..00000000 --- a/src/objects/setup_configuration/steps/objecttypes.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import IntegrityError - -from django_setup_configuration.configuration import BaseConfigurationStep -from django_setup_configuration.exceptions import ConfigurationRunFailed -from zgw_consumers.contrib.setup_configuration.models import ( - SingleServiceConfigurationModel, -) -from zgw_consumers.models import Service - - -class ObjectTypesConnectionConfigurationStep( - BaseConfigurationStep[SingleServiceConfigurationModel] -): - config_model = SingleServiceConfigurationModel - verbose_name = "Objecttypes connection configuration" - - namespace = "objects_api_objecttypes_connection" - enable_setting = "objects_api_objecttypes_connection_config_enable" - - def execute(self, model: SingleServiceConfigurationModel) -> None: - service_kwargs = dict( - slug=model.identifier, - label=model.label, - api_type=model.api_type, - api_root=model.api_root, - api_connection_check_path=model.api_connection_check_path, - auth_type=model.auth_type, - client_id=model.client_id, - secret=model.secret, - header_key=model.header_key, - header_value=model.header_value, - nlx=model.nlx, - user_id=model.user_id, - user_representation=model.user_representation, - timeout=model.timeout, - ) - - service = Service(**service_kwargs) - - try: - Service.objects.update_or_create( - slug=service.slug, - defaults={ - key: value for key, value in service_kwargs.items() if key != "slug" - }, - ) - except IntegrityError as exception: - exception_message = ( - f"Failed configuring ObjectType connection {service.slug}." - ) - raise ConfigurationRunFailed(exception_message) from exception