From 5eb5cc7ed4a4981797d66418baba504bfdbb1a27 Mon Sep 17 00:00:00 2001 From: Artem Kotik Date: Sun, 22 Mar 2026 16:29:50 +0100 Subject: [PATCH] Enhance ConfigurationRequest validation and add API tests - Refactor validation logic in ConfigurationRequestSerializer to improve clarity and efficiency. - Introduce new test suite for ConfigurationRequest API endpoints, covering list, retrieve, create, and error scenarios. - Add fixtures for API client and authenticated API client to streamline testing. --- netbox_config_diff/api/serializers.py | 27 +-- tests/conftest.py | 14 ++ tests/factories.py | 13 ++ tests/test_configurtion_request.py | 229 ++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 12 deletions(-) create mode 100644 tests/test_configurtion_request.py diff --git a/netbox_config_diff/api/serializers.py b/netbox_config_diff/api/serializers.py index a1fb958..5e4e2e4 100644 --- a/netbox_config_diff/api/serializers.py +++ b/netbox_config_diff/api/serializers.py @@ -101,21 +101,24 @@ class Meta: def validate(self, data): if data.get("devices"): - if devices := data["devices"].filter(platform__platform_setting__isnull=True): + if devices := Device.objects.filter(id__in=[d.id for d in data["devices"]]).filter( + platform__platform_setting__isnull=True + ): platforms = {d.platform.name for d in devices} raise ValidationError({"devices": f"Assign PlatformSetting for platform(s): {', '.join(platforms)}"}) - if drivers := { - device.platform.platform_setting.driver - for device in data["devices"] - if device.platform.platform_setting.driver not in ACCEPTABLE_DRIVERS - }: - raise ValidationError({"devices": f"Driver(s) not supported: {', '.join(drivers)}"}) - - if devices := list(filter(lambda x: x.get_config_template() is None, data["devices"])): - raise ValidationError( - {"devices": f"Define config template for device(s): {', '.join(d.name for d in devices)}"} - ) + unsupported_drivers = { + d.platform.platform_setting.driver + for d in data["devices"] + if d.platform.platform_setting.driver not in ACCEPTABLE_DRIVERS + } + if unsupported_drivers: + raise ValidationError({"devices": f"Driver(s) not supported: {', '.join(unsupported_drivers)}"}) + + devices_without_template = [d for d in data["devices"] if d.get_config_template() is None] + if devices_without_template: + names = ", ".join(d.name for d in devices_without_template) + raise ValidationError({"devices": f"Define config template for device(s): {names}"}) return super().validate(data) diff --git a/tests/conftest.py b/tests/conftest.py index e56d098..10c13b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from django.db import connection from django.test.utils import setup_databases from faker import Faker +from rest_framework.test import APIClient from typing_extensions import Unpack from netbox_config_diff.compliance.base import ConfigDiffBase @@ -125,3 +126,16 @@ def factory(**fields: Unpack["DeviceDataClassData"]) -> "DeviceDataClassData": def devicedataclass_data(devicedataclass_factory: "DeviceDataClassDataFactory") -> ConplianceDeviceDataClass: data = devicedataclass_factory() return ConplianceDeviceDataClass(**data) + + +@pytest.fixture +def api_client(): + """Basic API client fixture.""" + return APIClient() + + +@pytest.fixture +def authenticated_api_client(api_client, admin_user): + """Authenticated API client fixture using admin user.""" + api_client.force_authenticate(user=admin_user) + return api_client diff --git a/tests/factories.py b/tests/factories.py index fc19491..310a889 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -94,6 +94,19 @@ class Meta: model = ConfigCompliance +class ConfigurationRequestFactory(DjangoModelFactory): + class Meta: + model = "netbox_config_diff.ConfigurationRequest" + + @factory.post_generation + def devices(obj, create, extracted, **kwargs): + if not create: + return + if extracted: + for device in extracted: + obj.devices.add(device) + + class DataSourceFactory(DjangoModelFactory): name = factory.Sequence(lambda n: f"datasource-{n}") source_url = factory.Sequence(lambda n: f"/tmp/{n}") diff --git a/tests/test_configurtion_request.py b/tests/test_configurtion_request.py new file mode 100644 index 0000000..f6fe527 --- /dev/null +++ b/tests/test_configurtion_request.py @@ -0,0 +1,229 @@ +from http import HTTPStatus +from typing import TYPE_CHECKING + +import pytest +from django.urls import reverse +from users.models import User + +from netbox_config_diff.choices import ConfigurationRequestStatusChoices +from tests.factories import ( + ConfigurationRequestFactory, + DeviceFactory, + PlatformSettingFactory, +) + +if TYPE_CHECKING: + pass + + +@pytest.mark.django_db() +def test_configuration_request_list(authenticated_api_client): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + ConfigurationRequestFactory.create(devices=[device]) + ConfigurationRequestFactory.create(devices=[device]) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + response = authenticated_api_client.get(url) + + assert response.status_code == HTTPStatus.OK + assert response.json()["count"] == 2 + assert len(response.json()["results"]) == 2 + + +@pytest.mark.django_db() +def test_configuration_request_retrieve(authenticated_api_client): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + cr = ConfigurationRequestFactory.create(devices=[device]) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-detail", args=[cr.pk]) + response = authenticated_api_client.get(url) + + assert response.status_code == HTTPStatus.OK + data = response.json() + assert data["id"] == cr.pk + assert data["status"]["value"] == ConfigurationRequestStatusChoices.CREATED + assert len(data["devices"]) == 1 + assert data["devices"][0]["id"] == device.pk + + +@pytest.mark.django_db() +def test_configuration_request_create_success(authenticated_api_client, admin_user): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device.pk], + "description": "Test configuration request", + "comments": "Test comments", + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.CREATED + data = response.json() + assert data["status"]["value"] == ConfigurationRequestStatusChoices.CREATED + assert data["description"] == "Test configuration request" + assert data["comments"] == "Test comments" + assert len(data["devices"]) == 1 + assert data["created_by"]["id"] == admin_user.pk + + +@pytest.mark.django_db() +def test_configuration_request_create_multiple_devices(authenticated_api_client): + device1 = DeviceFactory.create() + device2 = DeviceFactory.create() + PlatformSettingFactory.create(platform=device1.platform) + PlatformSettingFactory.create(platform=device2.platform) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device1.pk, device2.pk], + "description": "Multi-device request", + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.CREATED + data = response.json() + assert len(data["devices"]) == 2 + device_ids = {d["id"] for d in data["devices"]} + assert device1.pk in device_ids + assert device2.pk in device_ids + + +@pytest.mark.django_db() +def test_configuration_request_create_no_platform_setting(authenticated_api_client): + device = DeviceFactory.create() + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device.pk], + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.json() + assert "devices" in data + assert "Assign PlatformSetting" in str(data["devices"]) + + +@pytest.mark.django_db() +def test_configuration_request_create_unsupported_driver(authenticated_api_client): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform, driver="unsupported_driver") + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device.pk], + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.json() + assert "devices" in data + assert "Driver(s) not supported" in str(data["devices"]) + + +@pytest.mark.django_db() +def test_configuration_request_create_no_config_template(authenticated_api_client): + device = DeviceFactory.create(config_template=None) + PlatformSettingFactory.create(platform=device.platform) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device.pk], + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.json() + assert "devices" in data + assert "Define config template" in str(data["devices"]) + + +@pytest.mark.django_db() +def test_configuration_request_create_mixed_valid_invalid_devices(authenticated_api_client): + valid_device = DeviceFactory.create() + PlatformSettingFactory.create(platform=valid_device.platform) + invalid_device = DeviceFactory.create() + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [valid_device.pk, invalid_device.pk], + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.BAD_REQUEST + data = response.json() + assert "devices" in data + + +@pytest.mark.django_db() +def test_configuration_request_create_empty_devices(authenticated_api_client): + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [], + } + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.CREATED + data = response.json() + assert len(data["devices"]) == 0 + + +@pytest.mark.django_db() +def test_configuration_request_create_without_authentication(api_client): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = { + "devices": [device.pk], + } + response = api_client.post(url, payload, format="json") + assert response.status_code == HTTPStatus.FORBIDDEN + + +@pytest.mark.django_db() +def test_configuration_request_list_filtering(authenticated_api_client): + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + + cr = ConfigurationRequestFactory.create(devices=[device], status=ConfigurationRequestStatusChoices.CREATED) + ConfigurationRequestFactory.create(devices=[device], status=ConfigurationRequestStatusChoices.APPROVED) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + + response = authenticated_api_client.get(url, {"status": ConfigurationRequestStatusChoices.CREATED}) + + assert response.status_code == HTTPStatus.OK + data = response.json() + + assert any(r["id"] == cr.pk for r in data["results"]) + + +@pytest.mark.django_db() +def test_configuration_request_retrieve_nonexistent(authenticated_api_client): + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-detail", args=[9999]) + response = authenticated_api_client.get(url) + + assert response.status_code == HTTPStatus.NOT_FOUND + + +@pytest.mark.django_db() +def test_configuration_request_create_sets_created_by(authenticated_api_client, admin_user): + user2 = User.objects.create_user(username="user2", password="pass") + + device = DeviceFactory.create() + PlatformSettingFactory.create(platform=device.platform) + + url = reverse("plugins-api:netbox_config_diff-api:configurationrequest-list") + payload = {"devices": [device.pk]} + response = authenticated_api_client.post(url, payload, format="json") + + assert response.status_code == HTTPStatus.CREATED + data = response.json() + + assert data["created_by"]["id"] == admin_user.pk + assert data["created_by"]["id"] != user2.pk