diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index 62e3e3a22..b6401b2fa 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -6,7 +6,7 @@ from django.urls import reverse from circuits.choices import * from circuits.models import * from dcim.models import Cable, Interface, Site -from utilities.testing import ViewTestCases, create_test_device +from utilities.testing import ViewTestCases, create_tags, create_test_device class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -21,7 +21,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): Provider(name='Provider 3', slug='provider-3', asn=65003), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Provider X', @@ -106,7 +106,7 @@ class CircuitTestCase(ViewTestCases.PrimaryObjectViewTestCase): Circuit(cid='Circuit 3', provider=providers[0], type=circuittypes[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'cid': 'Circuit X', @@ -157,7 +157,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase): ProviderNetwork(name='Provider Network 3', provider=providers[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Provider Network X', diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index daba2a639..5da1fcb5b 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -12,7 +12,7 @@ from dcim.choices import * from dcim.constants import * from dcim.models import * from ipam.models import VLAN -from utilities.testing import ViewTestCases, create_test_device +from utilities.testing import ViewTestCases, create_tags, create_test_device class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase): @@ -109,7 +109,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase): Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Site X', @@ -242,7 +242,7 @@ class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase): RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'rack': rack.pk, @@ -298,7 +298,7 @@ class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase): Rack(name='Rack 3', site=sites[0]), )) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Rack X', @@ -413,7 +413,7 @@ class DeviceTypeTestCase( DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'manufacturer': manufacturers[1].pk, @@ -1021,7 +1021,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase): Device(name='Device 3', site=sites[0], rack=racks[0], device_type=devicetypes[0], device_role=deviceroles[0], platform=platforms[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device_type': devicetypes[1].pk, @@ -1201,7 +1201,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase): ConsolePort(device=device, name='Console Port 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1259,7 +1259,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): ConsoleServerPort(device=device, name='Console Server Port 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1315,7 +1315,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase): PowerPort(device=device, name='Power Port 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1383,7 +1383,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase): PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1452,7 +1452,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): ) VLAN.objects.bulk_create(vlans) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1539,7 +1539,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase): FrontPort(device=device, name='Front Port 3', rear_port=rearports[2]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1600,7 +1600,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase): RearPort(device=device, name='Rear Port 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1661,7 +1661,7 @@ class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase): DeviceBay(device=device, name='Device Bay 3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1701,7 +1701,7 @@ class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase): InventoryItem.objects.create(device=device, name='Inventory Item 2') InventoryItem.objects.create(device=device, name='Inventory Item 3') - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, @@ -1791,7 +1791,7 @@ class CableTestCase( Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save() Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save() - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') interface_ct = ContentType.objects.get_for_model(Interface) cls.form_data = { @@ -1918,7 +1918,7 @@ class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase): PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'), )) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'site': sites[1].pk, @@ -1966,7 +1966,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase): PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]), )) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Power Feed X', diff --git a/netbox/extras/tests/test_changelog.py b/netbox/extras/tests/test_changelog.py index 91868832c..33251473f 100644 --- a/netbox/extras/tests/test_changelog.py +++ b/netbox/extras/tests/test_changelog.py @@ -7,7 +7,7 @@ from dcim.models import Site from extras.choices import * from extras.models import CustomField, ObjectChange, Tag from utilities.testing import APITestCase -from utilities.testing.utils import post_data +from utilities.testing.utils import create_tags, post_data from utilities.testing.views import ModelViewTestCase @@ -38,7 +38,7 @@ class ChangeLogViewTest(ModelViewTestCase): cf_select.content_types.set([ct]) def test_create_object(self): - tags = self.create_tags('Tag 1', 'Tag 2') + tags = create_tags('Tag 1', 'Tag 2') form_data = { 'name': 'Site 1', 'slug': 'site-1', @@ -72,7 +72,7 @@ class ChangeLogViewTest(ModelViewTestCase): def test_update_object(self): site = Site(name='Site 1', slug='site-1') site.save() - tags = self.create_tags('Tag 1', 'Tag 2', 'Tag 3') + tags = create_tags('Tag 1', 'Tag 2', 'Tag 3') site.tags.set('Tag 1', 'Tag 2') form_data = { @@ -116,7 +116,7 @@ class ChangeLogViewTest(ModelViewTestCase): } ) site.save() - self.create_tags('Tag 1', 'Tag 2') + create_tags('Tag 1', 'Tag 2') site.tags.set('Tag 1', 'Tag 2') request = { diff --git a/netbox/extras/tests/test_tags.py b/netbox/extras/tests/test_tags.py index 39aae49dc..b827b28d9 100644 --- a/netbox/extras/tests/test_tags.py +++ b/netbox/extras/tests/test_tags.py @@ -2,7 +2,7 @@ from django.urls import reverse from rest_framework import status from dcim.models import Site -from utilities.testing import APITestCase +from utilities.testing import APITestCase, create_tags class TaggedItemTest(APITestCase): @@ -10,7 +10,7 @@ class TaggedItemTest(APITestCase): Test the application of Tags to and item (a Site, for example) upon creation (POST) and modification (PATCH). """ def test_create_tagged_item(self): - tags = self.create_tags("Foo", "Bar", "Baz") + tags = create_tags("Foo", "Bar", "Baz") data = { 'name': 'Test Site', 'slug': 'test-site', @@ -37,7 +37,7 @@ class TaggedItemTest(APITestCase): slug='test-site' ) site.tags.add("Foo", "Bar", "Baz") - self.create_tags("New Tag") + create_tags("New Tag") data = { 'tags': [ {"name": "Foo"}, diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 2704ac1ca..057f9383f 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -6,7 +6,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from ipam.choices import * from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, RouteTarget, Service, VLAN, VLANGroup, VRF from tenancy.models import Tenant -from utilities.testing import ViewTestCases +from utilities.testing import ViewTestCases, create_tags class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase): @@ -27,7 +27,7 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase): VRF(name='VRF 3', rd='65000:3'), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'VRF X', @@ -64,7 +64,7 @@ class RouteTargetTestCase(ViewTestCases.PrimaryObjectViewTestCase): ) Tenant.objects.bulk_create(tenants) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') route_targets = ( RouteTarget(name='65000:1001', tenant=tenants[0]), @@ -141,7 +141,7 @@ class AggregateTestCase(ViewTestCases.PrimaryObjectViewTestCase): Aggregate(prefix=IPNetwork('10.3.0.0/16'), rir=rirs[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'prefix': IPNetwork('10.99.0.0/16'), @@ -226,7 +226,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase): Prefix(prefix=IPNetwork('10.3.0.0/16'), vrf=vrfs[0], site=sites[0], role=roles[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'prefix': IPNetwork('192.0.2.0/24'), @@ -277,7 +277,7 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): IPAddress(address=IPNetwork('192.0.2.3/24'), vrf=vrfs[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'vrf': vrfs[1].pk, @@ -374,7 +374,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase): VLAN(group=vlangroups[0], vid=103, name='VLAN103', site=sites[0], role=roles[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'site': sites[1].pk, @@ -434,7 +434,7 @@ class ServiceTestCase( Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, ports=[103]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'device': device.pk, diff --git a/netbox/tenancy/tests/test_views.py b/netbox/tenancy/tests/test_views.py index 8ef7efb12..f45afc302 100644 --- a/netbox/tenancy/tests/test_views.py +++ b/netbox/tenancy/tests/test_views.py @@ -1,5 +1,5 @@ from tenancy.models import Tenant, TenantGroup -from utilities.testing import ViewTestCases +from utilities.testing import ViewTestCases, create_tags class TenantGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase): @@ -53,7 +53,7 @@ class TenantTestCase(ViewTestCases.PrimaryObjectViewTestCase): Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Tenant X', diff --git a/netbox/utilities/testing/__init__.py b/netbox/utilities/testing/__init__.py index 1c18a3481..c6634f22e 100644 --- a/netbox/utilities/testing/__init__.py +++ b/netbox/utilities/testing/__init__.py @@ -1,3 +1,4 @@ from .api import * +from .base import * from .utils import * from .views import * diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 132eea2ae..b57c273fd 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -9,8 +9,8 @@ from rest_framework.test import APIClient from extras.choices import ObjectChangeActionChoices from extras.models import ObjectChange from users.models import ObjectPermission, Token +from .base import ModelTestCase from .utils import disable_warnings -from .views import ModelTestCase __all__ = ( diff --git a/netbox/utilities/testing/base.py b/netbox/utilities/testing/base.py new file mode 100644 index 000000000..4ebb7ae09 --- /dev/null +++ b/netbox/utilities/testing/base.py @@ -0,0 +1,161 @@ +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import FieldDoesNotExist +from django.db.models import ManyToManyField +from django.forms.models import model_to_dict +from django.test import Client, TestCase as _TestCase +from netaddr import IPNetwork +from taggit.managers import TaggableManager + +from users.models import ObjectPermission +from utilities.permissions import resolve_permission_ct +from .utils import extract_form_failures + +__all__ = ( + 'ModelTestCase', + 'TestCase', +) + + +class TestCase(_TestCase): + user_permissions = () + + def setUp(self): + + # Create the test user and assign permissions + self.user = User.objects.create_user(username='testuser') + self.add_permissions(*self.user_permissions) + + # Initialize the test client + self.client = Client() + self.client.force_login(self.user) + + # + # Permissions management + # + + def add_permissions(self, *names): + """ + Assign a set of permissions to the test user. Accepts permission names in the form ._. + """ + for name in names: + ct, action = resolve_permission_ct(name) + obj_perm = ObjectPermission(name=name, actions=[action]) + obj_perm.save() + obj_perm.users.add(self.user) + obj_perm.object_types.add(ct) + + # + # Custom assertions + # + + def assertHttpStatus(self, response, expected_status): + """ + TestCase method. Provide more detail in the event of an unexpected HTTP response. + """ + err_message = None + # Construct an error message only if we know the test is going to fail + if response.status_code != expected_status: + if hasattr(response, 'data'): + # REST API response; pass the response data through directly + err = response.data + else: + # Attempt to extract form validation errors from the response HTML + form_errors = extract_form_failures(response.content) + err = form_errors or response.content or 'No data' + err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}" + self.assertEqual(response.status_code, expected_status, err_message) + + +class ModelTestCase(TestCase): + """ + Parent class for TestCases which deal with models. + """ + model = None + + def _get_queryset(self): + """ + Return a base queryset suitable for use in test methods. + """ + return self.model.objects.all() + + def prepare_instance(self, instance): + """ + Test cases can override this method to perform any necessary manipulation of an instance prior to its evaluation + against test data. For example, it can be used to decrypt a Secret's plaintext attribute. + """ + return instance + + def model_to_dict(self, instance, fields, api=False): + """ + Return a dictionary representation of an instance. + """ + # Prepare the instance and call Django's model_to_dict() to extract all fields + model_dict = model_to_dict(self.prepare_instance(instance), fields=fields) + + # Map any additional (non-field) instance attributes that were specified + for attr in fields: + if hasattr(instance, attr) and attr not in model_dict: + model_dict[attr] = getattr(instance, attr) + + for key, value in list(model_dict.items()): + try: + field = instance._meta.get_field(key) + except FieldDoesNotExist: + # Attribute is not a model field + continue + + # Handle ManyToManyFields + if value and type(field) in (ManyToManyField, TaggableManager): + + if field.related_model is ContentType: + model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value]) + else: + model_dict[key] = sorted([obj.pk for obj in value]) + + if api: + + # Replace ContentType numeric IDs with . + if type(getattr(instance, key)) is ContentType: + ct = ContentType.objects.get(pk=value) + model_dict[key] = f'{ct.app_label}.{ct.model}' + + # Convert IPNetwork instances to strings + elif type(value) is IPNetwork: + model_dict[key] = str(value) + + else: + + # Convert ArrayFields to CSV strings + if type(instance._meta.get_field(key)) is ArrayField: + model_dict[key] = ','.join([str(v) for v in value]) + + return model_dict + + # + # Custom assertions + # + + def assertInstanceEqual(self, instance, data, exclude=None, api=False): + """ + Compare a model instance to a dictionary, checking that its attribute values match those specified + in the dictionary. + + :param instance: Python object instance + :param data: Dictionary of test data used to define the instance + :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed) + :param api: Set to True is the data is a JSON representation of the instance + """ + if exclude is None: + exclude = [] + + fields = [k for k in data.keys() if k not in exclude] + model_dict = self.model_to_dict(instance, fields=fields, api=api) + + # Omit any dictionary keys which are not instance attributes or have been excluded + relevant_data = { + k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude + } + + self.assertDictEqual(model_dict, relevant_data) diff --git a/netbox/utilities/testing/utils.py b/netbox/utilities/testing/utils.py index 2cf9795b5..bec3bc762 100644 --- a/netbox/utilities/testing/utils.py +++ b/netbox/utilities/testing/utils.py @@ -3,8 +3,10 @@ import re from contextlib import contextmanager from django.contrib.auth.models import Permission, User +from django.utils.text import slugify from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site +from extras.models import Tag def post_data(data): @@ -59,6 +61,15 @@ def create_test_user(username='testuser', permissions=None): return user +def create_tags(*names): + """ + Create and return a Tag instance for each name given. + """ + tags = [Tag(name=name, slug=slugify(name)) for name in names] + Tag.objects.bulk_create(tags) + return tags + + def extract_form_failures(content): """ Given raw HTML content from an HTTP response, return a list of form errors. diff --git a/netbox/utilities/testing/views.py b/netbox/utilities/testing/views.py index 6b1f4f8a9..4da724c7d 100644 --- a/netbox/utilities/testing/views.py +++ b/netbox/utilities/testing/views.py @@ -1,182 +1,20 @@ -from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType -from django.contrib.postgres.fields import ArrayField -from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist -from django.db.models import ManyToManyField -from django.forms.models import model_to_dict -from django.test import Client, TestCase as _TestCase, override_settings +from django.core.exceptions import ObjectDoesNotExist +from django.test import override_settings from django.urls import reverse -from django.utils.text import slugify -from netaddr import IPNetwork -from taggit.managers import TaggableManager from extras.choices import ObjectChangeActionChoices -from extras.models import ObjectChange, Tag +from extras.models import ObjectChange from users.models import ObjectPermission -from utilities.permissions import resolve_permission_ct -from .utils import disable_warnings, extract_form_failures, post_data - +from .base import ModelTestCase +from .utils import disable_warnings, post_data __all__ = ( - 'TestCase', - 'ModelTestCase', 'ModelViewTestCase', 'ViewTestCases', ) -class TestCase(_TestCase): - user_permissions = () - - def setUp(self): - - # Create the test user and assign permissions - self.user = User.objects.create_user(username='testuser') - self.add_permissions(*self.user_permissions) - - # Initialize the test client - self.client = Client() - self.client.force_login(self.user) - - def prepare_instance(self, instance): - """ - Test cases can override this method to perform any necessary manipulation of an instance prior to its evaluation - against test data. For example, it can be used to decrypt a Secret's plaintext attribute. - """ - return instance - - def model_to_dict(self, instance, fields, api=False): - """ - Return a dictionary representation of an instance. - """ - # Prepare the instance and call Django's model_to_dict() to extract all fields - model_dict = model_to_dict(self.prepare_instance(instance), fields=fields) - - # Map any additional (non-field) instance attributes that were specified - for attr in fields: - if hasattr(instance, attr) and attr not in model_dict: - model_dict[attr] = getattr(instance, attr) - - for key, value in list(model_dict.items()): - try: - field = instance._meta.get_field(key) - except FieldDoesNotExist: - # Attribute is not a model field - continue - - # Handle ManyToManyFields - if value and type(field) in (ManyToManyField, TaggableManager): - - if field.related_model is ContentType: - model_dict[key] = sorted([f'{ct.app_label}.{ct.model}' for ct in value]) - else: - model_dict[key] = sorted([obj.pk for obj in value]) - - if api: - - # Replace ContentType numeric IDs with . - if type(getattr(instance, key)) is ContentType: - ct = ContentType.objects.get(pk=value) - model_dict[key] = f'{ct.app_label}.{ct.model}' - - # Convert IPNetwork instances to strings - elif type(value) is IPNetwork: - model_dict[key] = str(value) - - else: - - # Convert ArrayFields to CSV strings - if type(instance._meta.get_field(key)) is ArrayField: - model_dict[key] = ','.join([str(v) for v in value]) - - return model_dict - - # - # Permissions management - # - - def add_permissions(self, *names): - """ - Assign a set of permissions to the test user. Accepts permission names in the form ._. - """ - for name in names: - ct, action = resolve_permission_ct(name) - obj_perm = ObjectPermission(name=name, actions=[action]) - obj_perm.save() - obj_perm.users.add(self.user) - obj_perm.object_types.add(ct) - - # - # Custom assertions - # - - def assertHttpStatus(self, response, expected_status): - """ - TestCase method. Provide more detail in the event of an unexpected HTTP response. - """ - err_message = None - # Construct an error message only if we know the test is going to fail - if response.status_code != expected_status: - if hasattr(response, 'data'): - # REST API response; pass the response data through directly - err = response.data - else: - # Attempt to extract form validation errors from the response HTML - form_errors = extract_form_failures(response.content) - err = form_errors or response.content or 'No data' - err_message = f"Expected HTTP status {expected_status}; received {response.status_code}: {err}" - self.assertEqual(response.status_code, expected_status, err_message) - - def assertInstanceEqual(self, instance, data, exclude=None, api=False): - """ - Compare a model instance to a dictionary, checking that its attribute values match those specified - in the dictionary. - - :param instance: Python object instance - :param data: Dictionary of test data used to define the instance - :param exclude: List of fields to exclude from comparison (e.g. passwords, which get hashed) - :param api: Set to True is the data is a JSON representation of the instance - """ - if exclude is None: - exclude = [] - - fields = [k for k in data.keys() if k not in exclude] - model_dict = self.model_to_dict(instance, fields=fields, api=api) - - # Omit any dictionary keys which are not instance attributes or have been excluded - relevant_data = { - k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude - } - - self.assertDictEqual(model_dict, relevant_data) - - # - # Convenience methods - # - - @classmethod - def create_tags(cls, *names): - """ - Create and return a Tag instance for each name given. - """ - tags = [Tag(name=name, slug=slugify(name)) for name in names] - Tag.objects.bulk_create(tags) - return tags - - -class ModelTestCase(TestCase): - """ - Parent class for TestCases which deal with models. - """ - model = None - - def _get_queryset(self): - """ - Return a base queryset suitable for use in test methods. - """ - return self.model.objects.all() - - # # UI Tests # @@ -185,7 +23,6 @@ class ModelViewTestCase(ModelTestCase): """ Base TestCase for model views. Subclass to test individual views. """ - def _get_base_url(self): """ Return the base format for a URL for the test's model. Override this to test for a model which belongs diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index c319f03ca..56c9cf280 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -5,7 +5,7 @@ from netaddr import EUI from dcim.choices import InterfaceModeChoices from dcim.models import DeviceRole, Platform, Site from ipam.models import VLAN -from utilities.testing import ViewTestCases +from utilities.testing import ViewTestCases, create_tags from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -100,7 +100,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase): Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'name': 'Cluster X', @@ -174,7 +174,7 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase): VirtualMachine(name='Virtual Machine 3', cluster=clusters[0], role=deviceroles[0], platform=platforms[0]), ]) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'cluster': clusters[1].pk, @@ -256,7 +256,7 @@ class VMInterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase): ) VLAN.objects.bulk_create(vlans) - tags = cls.create_tags('Alpha', 'Bravo', 'Charlie') + tags = create_tags('Alpha', 'Bravo', 'Charlie') cls.form_data = { 'virtual_machine': virtualmachines[1].pk,