From 5e327892f0511bd7438e0d4f8427f1a4f4d15165 Mon Sep 17 00:00:00 2001 From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu> Date: Thu, 16 Jan 2025 11:17:47 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=85=20Add=20API=20test=20for=20provid?= =?UTF-8?q?er=20type=20extra=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbox_sys_plugin/api/serializers.py | 10 + ...traconfig_assigned_object_type_and_more.py | 25 ++ netbox_sys_plugin/models/provider.py | 2 +- .../provider_type_extra_config/__init__.py | 0 .../test_provider_type_extra_config_api.py | 233 ++++++++++++++++++ 5 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 netbox_sys_plugin/migrations/0003_alter_providertypeextraconfig_assigned_object_type_and_more.py create mode 100644 netbox_sys_plugin/tests/provider_type_extra_config/__init__.py create mode 100644 netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_api.py diff --git a/netbox_sys_plugin/api/serializers.py b/netbox_sys_plugin/api/serializers.py index 2dfb905..84ed14d 100644 --- a/netbox_sys_plugin/api/serializers.py +++ b/netbox_sys_plugin/api/serializers.py @@ -123,11 +123,21 @@ class WebhookSettingsSerializer(NetBoxModelSerializer): class ProviderTypeExtraConfigSerializer(NetBoxModelSerializer): id = serializers.IntegerField(read_only=True) extra_config_structure = serializers.JSONField() + assigned_object_type = serializers.PrimaryKeyRelatedField( + queryset=ContentType.objects.all(), + write_only=True, + ) + display = serializers.CharField(source="__str__",read_only=True) class Meta: model = ProviderTypeExtraConfig fields = '__all__' + def create(self, validated_data): + assigned_object_type = validated_data.pop("assigned_object_type") + validated_data["assigned_object_type"] = assigned_object_type + return super().create(validated_data) + class VmAssignedExtraConfigSerializer(NetBoxModelSerializer): id = serializers.IntegerField(read_only=True) virtual_machine = VirtualMachineSerializer(source='assigned_object', read_only=True) diff --git a/netbox_sys_plugin/migrations/0003_alter_providertypeextraconfig_assigned_object_type_and_more.py b/netbox_sys_plugin/migrations/0003_alter_providertypeextraconfig_assigned_object_type_and_more.py new file mode 100644 index 0000000..f684ab6 --- /dev/null +++ b/netbox_sys_plugin/migrations/0003_alter_providertypeextraconfig_assigned_object_type_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.16 on 2025-01-16 11:16 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('netbox_sys_plugin', '0002_alter_providercredentials_assigned_object_type'), + ] + + operations = [ + migrations.AlterField( + model_name='providertypeextraconfig', + name='assigned_object_type', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'virtualization'), ('model', 'clustertype'))), null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'), + ), + migrations.AlterField( + model_name='virtualmachinetype', + name='assigned_object_type', + field=models.ForeignKey(blank=True, limit_choices_to=models.Q(models.Q(('app_label', 'virtualization'), ('model', 'clustertype'))), null=True, on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype'), + ), + ] diff --git a/netbox_sys_plugin/models/provider.py b/netbox_sys_plugin/models/provider.py index 12a7871..e6770e0 100644 --- a/netbox_sys_plugin/models/provider.py +++ b/netbox_sys_plugin/models/provider.py @@ -9,7 +9,7 @@ from netbox.models import NetBoxModel from ..validators import validate_extra_config_structure CLUSTER_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="cluster")) -CLUSTER_TYPE_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="ClusterType")) +CLUSTER_TYPE_ASSIGNMENT_MODELS = models.Q(models.Q(app_label="virtualization", model="clustertype")) #Provider Credentials class ProviderCredentials(NetBoxModel): diff --git a/netbox_sys_plugin/tests/provider_type_extra_config/__init__.py b/netbox_sys_plugin/tests/provider_type_extra_config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_api.py b/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_api.py new file mode 100644 index 0000000..fb3071f --- /dev/null +++ b/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_api.py @@ -0,0 +1,233 @@ +"""SYS Plugin Provider Type Extra Config API Test Case Class""" + +from users.models import ObjectPermission +from django.contrib.contenttypes.models import ContentType +from rest_framework import status +from virtualization.models import ClusterType +from netbox_sys_plugin.models import ProviderTypeExtraConfig +from ..base import BaseAPITestCase + +class ProviderTypeExtraConfigApiTestCase(BaseAPITestCase): + """Test suite for ProviderTypeExtraConfig API""" + model = ProviderTypeExtraConfig + brief_fields = ["extra_config_name","extra_config_structure","extra_config_description", "assigned_object_id", "assigned_object_type"] + + + @classmethod + def setUpTestData(cls): + + """Set up test data for ProviderTypeExtraConfig API""" + cls.ct_ct = ContentType.objects.get_for_model(ClusterType) + + # Create a ClusterType + cls.cluster_type = ClusterType.objects.create(name="Test ClusterType", slug="CT") + + # Create a ClusterType + cls.cluster_type2 = ClusterType.objects.create(name="Test ClusterType 2", slug="CT2") + + # Create a ClusterType + cls.cluster_type3 = ClusterType.objects.create(name="Test ClusterType 3", slug="CT3") + + # Create a ClusterType + cls.cluster_type4 = ClusterType.objects.create(name="Test ClusterType 4", slug="CT4") + + # Create a ClusterType + cls.cluster_type5 = ClusterType.objects.create(name="Test ClusterType 5", slug="CT5") + + # Create a ClusterType + cls.cluster_type6 = ClusterType.objects.create(name="Test ClusterType 6", slug="CT6") + + # Create a ClusterType + cls.cluster_type7 = ClusterType.objects.create(name="Test ClusterType 7", slug="CT8") + + # Create a ClusterType + cls.cluster_type8 = ClusterType.objects.create(name="Test ClusterType 8", slug="CT7") + + # Create Extra Configs entries linked to the Cluster Types + ProviderTypeExtraConfig.objects.create( + extra_config_name="TEST_1", + extra_config_structure={'test_extra1': [{'id': {'required': 'true','type': 'String'}}]}, + extra_config_description="Test Extra1", + assigned_object=cls.cluster_type7 + ) + ProviderTypeExtraConfig.objects.create( + extra_config_name="TEST_2", + extra_config_structure={'test_extra2': [{'id': {'required': 'true','type': 'String'}}]}, + extra_config_description="Test Extra2", + assigned_object=cls.cluster_type8 + ) + + # Data for valid creation + cls.valid_create_data = [ + { + "extra_config_name": "Test Config", + "extra_config_structure": {'test_valid': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test structure", + "assigned_object_type": cls.ct_ct.id, + "assigned_object_id": cls.cluster_type.pk, + } + ] + + # Data for invalid creation + cls.invalid_create_data = [ + { + "extra_config_name": "Config Invalid 1", + "extra_config_structure": {'test_invalid': {'id': {'required': 'true'}}}, #No dict root key + "extra_config_description": "Test structure", + "assigned_object_id": cls.cluster_type2.pk, + "assigned_object_type": cls.ct_ct.id, + }, + { + "extra_config_name": "Config Invalid 2", + "extra_config_structure": {'test_valid': [{'id': {'type': 'String'}}]}, #No required field + "extra_config_description": "Test structure", + "assigned_object_id": cls.cluster_type3.pk, + "assigned_object_type": cls.ct_ct.id, + + }, + { + "extra_config_name": "Config Invalid 4", + "extra_config_structure": {'test_valid': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test structure", + "assigned_object_id": cls.cluster_type5.pk, + "assigned_object_type": None #No assignment field + }, + ] + + # Data for checking unique key + cls.valid_check_unique_data = [ + { + "extra_config_name": "Test Config uniq", + "extra_config_structure": {'test_uniq': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test structure", + "assigned_object_type": cls.ct_ct.pk, + "assigned_object_id": cls.cluster_type6.id, # Same Virtual Machine + }, + { + "extra_config_name": "Test Config uniq2", + "extra_config_structure": {'test_uniq2': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test structure", + "assigned_object_type": cls.ct_ct.pk, + "assigned_object_id": cls.cluster_type6.id, # Same Virtual Machine + } + ] + + def test_create_valid_extra_config(self): + """Test creating a valid extra config""" + obj_perm = ObjectPermission( + name="Create Extra Config Permission", + actions=["add", "view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + form_data = self.valid_create_data[0] + response = self.client.post(self._get_list_url(), form_data, format="json", **self.header) + + self.assertHttpStatus(response, status.HTTP_201_CREATED) + self.assertEqual(response.data["extra_config_name"], form_data["extra_config_name"]) + self.assertEqual(response.data["extra_config_structure"], form_data["extra_config_structure"]) + self.assertEqual(response.data["extra_config_description"], form_data["extra_config_description"]) + + def test_extra_config_unique_key(self): + """Test extra config unique key""" + obj_perm = ObjectPermission( + name="Invalid Extra Config Permission", + actions=["add", "view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + for form_data in self.valid_check_unique_data: + response = self.client.post(self._get_list_url(), form_data, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('The fields assigned_object_id must make a unique set.',str(response.data)) + + def test_create_invalid_extra_config(self): + """Test creating invalid extra config""" + obj_perm = ObjectPermission( + name="Invalid Extra Config Permission", + actions=["add", "view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + form_data = self.invalid_create_data[0] + response = self.client.post(self._get_list_url(), form_data, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('the value of the root key must be a list',str(response.data)) + + form_data = self.invalid_create_data[1] + response = self.client.post(self._get_list_url(), form_data, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('must contain `type` and `required` keys',str(response.data)) + + form_data = self.invalid_create_data[2] + response = self.client.post(self._get_list_url(), form_data, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('This field may not be null.',str(response.data)) + + def test_update_extra_config(self): + """Test updating an existing extra config""" + extra_config = ProviderTypeExtraConfig.objects.first() + obj_perm = ObjectPermission( + name="Update Extra Config Permission", + actions=["change", "view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + update_data = {"extra_config_name": "Update Data"} + response = self.client.patch(self._get_detail_url(extra_config), update_data, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data["extra_config_name"], update_data["extra_config_name"]) + + def test_get_all_extra_config(self): + """Test fetching all extra config""" + obj_perm = ObjectPermission( + name="Create Extra Config Permission", + actions=["view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + response = self.client.get(self._get_list_url(), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertGreaterEqual(len(response.data), 2) + + def test_get_single_extra_config(self): + """Test fetching a single extra config""" + obj_perm = ObjectPermission( + name="View Extra Config Permission", + actions=["view"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + extra_config = ProviderTypeExtraConfig.objects.first() + response = self.client.get(self._get_detail_url(extra_config), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertEqual(response.data["extra_config_name"], extra_config.extra_config_name) + self.assertEqual(response.data["extra_config_structure"], extra_config.extra_config_structure) + self.assertEqual(response.data["extra_config_description"], extra_config.extra_config_description) + + def test_delete_extra_config(self): + """Test deleting a extra config""" + extra_config = ProviderTypeExtraConfig.objects.first() + obj_perm = ObjectPermission( + name="Delete Extra Config Permission", + actions=["delete"], + ) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ContentType.objects.get_for_model(ProviderTypeExtraConfig)) + + response = self.client.delete(self._get_detail_url(extra_config), **self.header) + self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) + self.assertFalse(ProviderTypeExtraConfig.objects.filter(id=extra_config.id).exists()) -- GitLab From ea4a36d32203b12d3754ccb0c232435153812f1c Mon Sep 17 00:00:00 2001 From: Frederico Sequeira <frederico.sequeira@ext.ec.europa.eu> Date: Thu, 16 Jan 2025 17:37:24 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20Add=20view=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_provider_type_extra_config_view.py | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_view.py diff --git a/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_view.py b/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_view.py new file mode 100644 index 0000000..243f050 --- /dev/null +++ b/netbox_sys_plugin/tests/provider_type_extra_config/test_provider_type_extra_config_view.py @@ -0,0 +1,168 @@ +"""VM Maintenance Views Test Case Class""" + +from django.contrib.contenttypes.models import ContentType +from users.models import ObjectPermission +from virtualization.models import ClusterType +from netbox_sys_plugin.models import ProviderTypeExtraConfig +from netbox_sys_plugin.forms import ProviderTypeExtraConfigForm +from .. base import BaseModelViewTestCase + + + +class ProviderTypeExtraConfigFormTestCase( + BaseModelViewTestCase, +): + """Provider Type Extra Config Test Case Class""" + + model = ProviderTypeExtraConfig + form = ProviderTypeExtraConfigForm + + def test_create_valid_extra_config(self): + """Create a valid Extra Config""" + + cluster_type1 = ClusterType.objects.create(name="Test ClusterType3", slug="ClusterType3") + + form = ProviderTypeExtraConfigForm(data= { + "extra_config_name": "test_config_1", + "extra_config_structure": {'test_extra1': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test Config 1", + "cluster_type": cluster_type1.pk + }) + self.assertTrue(form.is_valid()) + # Setup object permissions for the test user + obj_perm = ObjectPermission( + name='Test permission', + actions=['add', 'change'] + ) + obj_perm.save() + obj_perm.users.add(self.user) # pylint: disable=no-member + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) # pylint: disable=no-member + + def test_create_cluster_type_with_two_assigment(self): + """Test the assignment of 2 extra configs to the same Cluster Type """ + # pylint: disable=W0201 + self.clt_content_type = ContentType.objects.get_for_model(ClusterType) + # pylint: disable=W0201 + self.cluster_type = ClusterType.objects.create(name="Test ClusterType3", slug="ClusterType3") + # pylint: disable=W0201 + self.extra_config1 = ProviderTypeExtraConfig.objects.create( + extra_config_name='test_config_3', + extra_config_structure={"test_extra3": [{"id": {"required": "true","type": "String"}}]}, + extra_config_description='Test Config 3', + + assigned_object_type=self.clt_content_type, + assigned_object_id=self.cluster_type.pk + ) + + form = ProviderTypeExtraConfigForm(data= { + "extra_config_name": "test_config_2", + "extra_config_structure": {"test_extra3": [{"id": {"required": "true","type": "String"}}]}, + "extra_config_description": "Test Config 2", + "cluster_type": self.cluster_type.pk, + }) + + self.assertFalse(form.is_valid()) + # Setup object permissions for the test user + obj_perm = ObjectPermission( + name='Test permission', + actions=['add', 'change'] + ) + obj_perm.save() + obj_perm.users.add(self.user) # pylint: disable=no-member + obj_perm.object_types.add(ContentType.objects.get_for_model(self.model)) # pylint: disable=no-member + + self.assertIn( + "A Provider Type can only have one extra configuration", + form.errors.get("__all__",[]) + ) + + def test_invalid_extra_config_format(self): + """ + Test invalid maintenance window invalid format. + """ + + # Create VLAN and VLANGroup for testing + # pylint: disable=W0201 + self.vm_content_type = ContentType.objects.get_for_model(ClusterType) + # pylint: disable=W0201 + self.cluster_type = ClusterType.objects.create(name="Test ClusterType3", slug="ClusterType3") + + # Set up valid form data + self.invalid_valid_form_data = { + "extra_config_name": "test_config_format1", + "extra_config_structure": {'test_extra1': [{'id': {'required': 'true','type': 'String'}}]}, + "extra_config_description": "Test Config format1", + "cluster_type": self.cluster_type.pk + } + #Invalid JSON + invalid_form_data = self.invalid_valid_form_data.copy() + invalid_form_data["extra_config_structure"] = "Invalid" # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "Enter a valid JSON.", + form.errors.get("extra_config_structure", []), + ) + #List Validation + invalid_form_data["extra_config_structure"] = [{'test_structure': [{'field1': {'type': 'string', 'required': True}}]}] # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "The structure must be a dictionary.", + form.errors.get("extra_config_structure", []), + ) + #Missing property Type + invalid_form_data["extra_config_structure"] = {'structure1': [], 'structure2': []} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "The structure must contain exactly one key.", + form.errors.get("extra_config_structure", []), + ) + #Missing property Required + invalid_form_data["extra_config_structure"] = {'test_structure': {'field1': {'type': 'string', 'required': True}}} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "For test_structure the value of the root key must be a list.", + form.errors.get("extra_config_structure", []), + ) + + #Dict Validation + invalid_form_data["extra_config_structure"] = {'test_structure': ['field1']} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "Each field in the list must be a dictionary.", + form.errors.get("extra_config_structure", []), + ) + #Only one key + invalid_form_data["extra_config_structure"] = {'test_structure': [{'field1': 'string'}]} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "The field `field1` must be a dictionary.", + form.errors.get("extra_config_structure", []), + ) + #Missing property Type + invalid_form_data["extra_config_structure"] = {'test_structure': [{'field1': {'required': True}}]} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "The field `field1` must contain `type` and `required` keys.", + form.errors.get("extra_config_structure", []), + ) + + #Missing property Required + invalid_form_data["extra_config_structure"] = {'test_structure': [{'field1': {'type': 'string'}}]} # Invalid format + form = self.form(data=invalid_form_data) + self.assertFalse(form.is_valid()) + self.assertIn( + "The field `field1` must contain `type` and `required` keys.", + form.errors.get("extra_config_structure", []), + ) + + def tearDown(self) -> None:# pylint: disable=invalid-name + """Method called immediately after the test method has been called and the result recorded.""" + ProviderTypeExtraConfig.objects.all().delete() + super().tearDown() -- GitLab