2
0
Эх сурвалжийг харах

Merge pull request #4072 from netbox-community/4000-view-tests

Closes #4000: Add tests for the create, edit, and delete views of all models
Jeremy Stretch 6 жил өмнө
parent
commit
ce081a6e15

+ 47 - 93
netbox/circuits/tests/test_views.py

@@ -1,23 +1,15 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
+import datetime
 
 
+from circuits.choices import *
 from circuits.models import Circuit, CircuitType, Provider
 from circuits.models import Circuit, CircuitType, Provider
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
 
 
-class ProviderTestCase(TestCase):
+class ProviderTestCase(StandardTestCases.Views):
+    model = Provider
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_provider',
-                'circuits.add_provider',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         Provider.objects.bulk_create([
         Provider.objects.bulk_create([
             Provider(name='Provider 1', slug='provider-1', asn=65001),
             Provider(name='Provider 1', slug='provider-1', asn=65001),
@@ -25,48 +17,35 @@ class ProviderTestCase(TestCase):
             Provider(name='Provider 3', slug='provider-3', asn=65003),
             Provider(name='Provider 3', slug='provider-3', asn=65003),
         ])
         ])
 
 
-    def test_provider_list(self):
-
-        url = reverse('circuits:provider_list')
-        params = {
-            "q": "test",
+        cls.form_data = {
+            'name': 'Provider X',
+            'slug': 'provider-x',
+            'asn': 65123,
+            'account': '1234',
+            'portal_url': 'http://example.com/portal',
+            'noc_contact': 'noc@example.com',
+            'admin_contact': 'admin@example.com',
+            'comments': 'Another provider',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_provider(self):
-
-        provider = Provider.objects.first()
-        response = self.client.get(provider.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_provider_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Provider 4,provider-4",
             "Provider 4,provider-4",
             "Provider 5,provider-5",
             "Provider 5,provider-5",
             "Provider 6,provider-6",
             "Provider 6,provider-6",
         )
         )
 
 
-        response = self.client.post(reverse('circuits:provider_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Provider.objects.count(), 6)
 
 
+class CircuitTypeTestCase(StandardTestCases.Views):
+    model = CircuitType
 
 
-class CircuitTypeTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_circuittype',
-                'circuits.add_circuittype',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         CircuitType.objects.bulk_create([
         CircuitType.objects.bulk_create([
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
             CircuitType(name='Circuit Type 1', slug='circuit-type-1'),
@@ -74,39 +53,25 @@ class CircuitTypeTestCase(TestCase):
             CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
             CircuitType(name='Circuit Type 3', slug='circuit-type-3'),
         ])
         ])
 
 
-    def test_circuittype_list(self):
-
-        url = reverse('circuits:circuittype_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuittype_import(self):
+        cls.form_data = {
+            'name': 'Circuit Type X',
+            'slug': 'circuit-type-x',
+            'description': 'A new circuit type',
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Circuit Type 4,circuit-type-4",
             "Circuit Type 4,circuit-type-4",
             "Circuit Type 5,circuit-type-5",
             "Circuit Type 5,circuit-type-5",
             "Circuit Type 6,circuit-type-6",
             "Circuit Type 6,circuit-type-6",
         )
         )
 
 
-        response = self.client.post(reverse('circuits:circuittype_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(CircuitType.objects.count(), 6)
-
 
 
-class CircuitTestCase(TestCase):
+class CircuitTestCase(StandardTestCases.Views):
+    model = Circuit
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'circuits.view_circuit',
-                'circuits.add_circuit',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider = Provider(name='Provider 1', slug='provider-1', asn=65001)
         provider.save()
         provider.save()
@@ -120,33 +85,22 @@ class CircuitTestCase(TestCase):
             Circuit(cid='Circuit 3', provider=provider, type=circuittype),
             Circuit(cid='Circuit 3', provider=provider, type=circuittype),
         ])
         ])
 
 
-    def test_circuit_list(self):
-
-        url = reverse('circuits:circuit_list')
-        params = {
-            "provider": Provider.objects.first().slug,
-            "type": CircuitType.objects.first().slug,
+        cls.form_data = {
+            'cid': 'Circuit X',
+            'provider': provider.pk,
+            'type': circuittype.pk,
+            'status': CircuitStatusChoices.STATUS_ACTIVE,
+            'tenant': None,
+            'install_date': datetime.date(2020, 1, 1),
+            'commit_rate': 1000,
+            'description': 'A new circuit',
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuit(self):
-
-        circuit = Circuit.objects.first()
-        response = self.client.get(circuit.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_circuit_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "cid,provider,type",
             "cid,provider,type",
             "Circuit 4,Provider 1,Circuit Type 1",
             "Circuit 4,Provider 1,Circuit Type 1",
             "Circuit 5,Provider 1,Circuit Type 1",
             "Circuit 5,Provider 1,Circuit Type 1",
             "Circuit 6,Provider 1,Circuit Type 1",
             "Circuit 6,Provider 1,Circuit Type 1",
         )
         )
-
-        response = self.client.post(reverse('circuits:circuit_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Circuit.objects.count(), 6)

+ 1 - 0
netbox/dcim/forms.py

@@ -1549,6 +1549,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
     )
     )
     manufacturer = forms.ModelChoiceField(
     manufacturer = forms.ModelChoiceField(
         queryset=Manufacturer.objects.all(),
         queryset=Manufacturer.objects.all(),
+        required=False,
         widget=APISelect(
         widget=APISelect(
             api_url="/api/dcim/manufacturers/",
             api_url="/api/dcim/manufacturers/",
             filter_for={
             filter_for={

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 296 - 376
netbox/dcim/tests/test_views.py


+ 51 - 43
netbox/extras/tests/test_views.py

@@ -2,48 +2,55 @@ import urllib.parse
 import uuid
 import uuid
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Site
 from dcim.models import Site
 from extras.choices import ObjectChangeActionChoices
 from extras.choices import ObjectChangeActionChoices
 from extras.models import ConfigContext, ObjectChange, Tag
 from extras.models import ConfigContext, ObjectChange, Tag
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases, TestCase
 
 
 
 
-class TagTestCase(TestCase):
+class TagTestCase(StandardTestCases.Views):
+    model = Tag
 
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_tag'])
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_create_object = None
+    test_import_objects = None
 
 
-        Tag.objects.bulk_create([
+    # TODO: Restore test when #4071 is resolved
+    test_get_object = None
+
+    @classmethod
+    def setUpTestData(cls):
+
+        Tag.objects.bulk_create((
             Tag(name='Tag 1', slug='tag-1'),
             Tag(name='Tag 1', slug='tag-1'),
             Tag(name='Tag 2', slug='tag-2'),
             Tag(name='Tag 2', slug='tag-2'),
             Tag(name='Tag 3', slug='tag-3'),
             Tag(name='Tag 3', slug='tag-3'),
-        ])
+        ))
 
 
-    def test_tag_list(self):
-
-        url = reverse('extras:tag_list')
-        params = {
-            "q": "tag",
+        cls.form_data = {
+            'name': 'Tag X',
+            'slug': 'tag-x',
+            'color': 'c0c0c0',
+            'comments': 'Some comments',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
 
 
+class ConfigContextTestCase(StandardTestCases.Views):
+    model = ConfigContext
 
 
-class ConfigContextTestCase(TestCase):
+    # Disable inapplicable tests
+    test_import_objects = None
 
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_configcontext'])
-        self.client = Client()
-        self.client.force_login(user)
+    # TODO: Resolve model discrepancies when creating/editing ConfigContexts
+    test_create_object = None
+    test_edit_object = None
 
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+    @classmethod
+    def setUpTestData(cls):
+
+        site = Site.objects.create(name='Site 1', slug='site-1')
 
 
         # Create three ConfigContexts
         # Create three ConfigContexts
         for i in range(1, 4):
         for i in range(1, 4):
@@ -54,34 +61,35 @@ class ConfigContextTestCase(TestCase):
             configcontext.save()
             configcontext.save()
             configcontext.sites.add(site)
             configcontext.sites.add(site)
 
 
-    def test_configcontext_list(self):
-
-        url = reverse('extras:configcontext_list')
-        params = {
-            "q": "foo",
+        cls.form_data = {
+            'name': 'Config Context X',
+            'weight': 200,
+            'description': 'A new config context',
+            'is_active': True,
+            'regions': [],
+            'sites': [site.pk],
+            'roles': [],
+            'platforms': [],
+            'tenant_groups': [],
+            'tenants': [],
+            'tags': [],
+            'data': '{"foo": 123}',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_configcontext(self):
-
-        configcontext = ConfigContext.objects.first()
-        response = self.client.get(configcontext.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
 
 
 class ObjectChangeTestCase(TestCase):
 class ObjectChangeTestCase(TestCase):
+    user_permissions = (
+        'extras.view_objectchange',
+    )
 
 
-    def setUp(self):
-        user = create_test_user(permissions=['extras.view_objectchange'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         site = Site(name='Site 1', slug='site-1')
         site = Site(name='Site 1', slug='site-1')
         site.save()
         site.save()
 
 
         # Create three ObjectChanges
         # Create three ObjectChanges
+        user = User.objects.create_user(username='testuser2')
         for i in range(1, 4):
         for i in range(1, 4):
             oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
             oc = site.to_objectchange(action=ObjectChangeActionChoices.ACTION_UPDATE)
             oc.user = user
             oc.user = user
@@ -96,10 +104,10 @@ class ObjectChangeTestCase(TestCase):
         }
         }
 
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
 
     def test_objectchange(self):
     def test_objectchange(self):
 
 
         objectchange = ObjectChange.objects.first()
         objectchange = ObjectChange.objects.first()
         response = self.client.get(objectchange.get_absolute_url())
         response = self.client.get(objectchange.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)

+ 146 - 278
netbox/ipam/tests/test_views.py

@@ -1,26 +1,18 @@
-from netaddr import IPNetwork
-import urllib.parse
+import datetime
 
 
-from django.test import Client, TestCase
-from django.urls import reverse
+from netaddr import IPNetwork
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.choices import ServiceProtocolChoices
+from ipam.choices import *
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
 
 
-class VRFTestCase(TestCase):
+class VRFTestCase(StandardTestCases.Views):
+    model = VRF
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vrf',
-                'ipam.add_vrf',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         VRF.objects.bulk_create([
         VRF.objects.bulk_create([
             VRF(name='VRF 1', rd='65000:1'),
             VRF(name='VRF 1', rd='65000:1'),
@@ -28,48 +20,32 @@ class VRFTestCase(TestCase):
             VRF(name='VRF 3', rd='65000:3'),
             VRF(name='VRF 3', rd='65000:3'),
         ])
         ])
 
 
-    def test_vrf_list(self):
-
-        url = reverse('ipam:vrf_list')
-        params = {
-            "q": "65000",
+        cls.form_data = {
+            'name': 'VRF X',
+            'rd': '65000:999',
+            'tenant': None,
+            'enforce_unique': True,
+            'description': 'A new VRF',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vrf(self):
-
-        vrf = VRF.objects.first()
-        response = self.client.get(vrf.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_vrf_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name",
             "name",
             "VRF 4",
             "VRF 4",
             "VRF 5",
             "VRF 5",
             "VRF 6",
             "VRF 6",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:vrf_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VRF.objects.count(), 6)
+class RIRTestCase(StandardTestCases.Views):
+    model = RIR
 
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
 
-class RIRTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_rir',
-                'ipam.add_rir',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         RIR.objects.bulk_create([
         RIR.objects.bulk_create([
             RIR(name='RIR 1', slug='rir-1'),
             RIR(name='RIR 1', slug='rir-1'),
@@ -77,42 +53,27 @@ class RIRTestCase(TestCase):
             RIR(name='RIR 3', slug='rir-3'),
             RIR(name='RIR 3', slug='rir-3'),
         ])
         ])
 
 
-    def test_rir_list(self):
-
-        url = reverse('ipam:rir_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_rir_import(self):
+        cls.form_data = {
+            'name': 'RIR X',
+            'slug': 'rir-x',
+            'is_private': True,
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "RIR 4,rir-4",
             "RIR 4,rir-4",
             "RIR 5,rir-5",
             "RIR 5,rir-5",
             "RIR 6,rir-6",
             "RIR 6,rir-6",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:rir_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(RIR.objects.count(), 6)
 
 
+class AggregateTestCase(StandardTestCases.Views):
+    model = Aggregate
 
 
-class AggregateTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_aggregate',
-                'ipam.add_aggregate',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
-        rir = RIR(name='RIR 1', slug='rir-1')
-        rir.save()
+        rir = RIR.objects.create(name='RIR 1', slug='rir-1')
 
 
         Aggregate.objects.bulk_create([
         Aggregate.objects.bulk_create([
             Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
             Aggregate(family=4, prefix=IPNetwork('10.1.0.0/16'), rir=rir),
@@ -120,48 +81,32 @@ class AggregateTestCase(TestCase):
             Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
             Aggregate(family=4, prefix=IPNetwork('10.3.0.0/16'), rir=rir),
         ])
         ])
 
 
-    def test_aggregate_list(self):
-
-        url = reverse('ipam:aggregate_list')
-        params = {
-            "rir": RIR.objects.first().slug,
+        cls.form_data = {
+            'family': 4,
+            'prefix': IPNetwork('10.99.0.0/16'),
+            'rir': rir.pk,
+            'date_added': datetime.date(2020, 1, 1),
+            'description': 'A new aggregate',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_aggregate(self):
-
-        aggregate = Aggregate.objects.first()
-        response = self.client.get(aggregate.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_aggregate_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "prefix,rir",
             "prefix,rir",
             "10.4.0.0/16,RIR 1",
             "10.4.0.0/16,RIR 1",
             "10.5.0.0/16,RIR 1",
             "10.5.0.0/16,RIR 1",
             "10.6.0.0/16,RIR 1",
             "10.6.0.0/16,RIR 1",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:aggregate_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Aggregate.objects.count(), 6)
 
 
+class RoleTestCase(StandardTestCases.Views):
+    model = Role
 
 
-class RoleTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_role',
-                'ipam.add_role',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         Role.objects.bulk_create([
         Role.objects.bulk_create([
             Role(name='Role 1', slug='role-1'),
             Role(name='Role 1', slug='role-1'),
@@ -169,42 +114,31 @@ class RoleTestCase(TestCase):
             Role(name='Role 3', slug='role-3'),
             Role(name='Role 3', slug='role-3'),
         ])
         ])
 
 
-    def test_role_list(self):
-
-        url = reverse('ipam:role_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_role_import(self):
+        cls.form_data = {
+            'name': 'Role X',
+            'slug': 'role-x',
+            'weight': 200,
+            'description': 'A new role',
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug,weight",
             "name,slug,weight",
             "Role 4,role-4,1000",
             "Role 4,role-4,1000",
             "Role 5,role-5,1000",
             "Role 5,role-5,1000",
             "Role 6,role-6,1000",
             "Role 6,role-6,1000",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:role_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Role.objects.count(), 6)
 
 
+class PrefixTestCase(StandardTestCases.Views):
+    model = Prefix
 
 
-class PrefixTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_prefix',
-                'ipam.add_prefix',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
+        role = Role.objects.create(name='Role 1', slug='role-1')
+        # vlan = VLAN.objects.create(vid=123, name='VLAN 123')
 
 
         Prefix.objects.bulk_create([
         Prefix.objects.bulk_create([
             Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
             Prefix(family=4, prefix=IPNetwork('10.1.0.0/16'), site=site),
@@ -212,51 +146,34 @@ class PrefixTestCase(TestCase):
             Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
             Prefix(family=4, prefix=IPNetwork('10.3.0.0/16'), site=site),
         ])
         ])
 
 
-    def test_prefix_list(self):
-
-        url = reverse('ipam:prefix_list')
-        params = {
-            "site": Site.objects.first().slug,
+        cls.form_data = {
+            'prefix': IPNetwork('192.0.2.0/24'),
+            'site': site.pk,
+            'vrf': vrf.pk,
+            'tenant': None,
+            'vlan': None,
+            'status': PrefixStatusChoices.STATUS_RESERVED,
+            'role': role.pk,
+            'is_pool': True,
+            'description': 'A new prefix',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_prefix(self):
-
-        prefix = Prefix.objects.first()
-        response = self.client.get(prefix.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_prefix_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "prefix,status",
             "prefix,status",
             "10.4.0.0/16,Active",
             "10.4.0.0/16,Active",
             "10.5.0.0/16,Active",
             "10.5.0.0/16,Active",
             "10.6.0.0/16,Active",
             "10.6.0.0/16,Active",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:prefix_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Prefix.objects.count(), 6)
 
 
+class IPAddressTestCase(StandardTestCases.Views):
+    model = IPAddress
 
 
-class IPAddressTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_ipaddress',
-                'ipam.add_ipaddress',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        vrf = VRF(name='VRF 1', rd='65000:1')
-        vrf.save()
+        vrf = VRF.objects.create(name='VRF 1', rd='65000:1')
 
 
         IPAddress.objects.bulk_create([
         IPAddress.objects.bulk_create([
             IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf),
             IPAddress(family=4, address=IPNetwork('192.0.2.1/24'), vrf=vrf),
@@ -264,51 +181,38 @@ class IPAddressTestCase(TestCase):
             IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf),
             IPAddress(family=4, address=IPNetwork('192.0.2.3/24'), vrf=vrf),
         ])
         ])
 
 
-    def test_ipaddress_list(self):
-
-        url = reverse('ipam:ipaddress_list')
-        params = {
-            "vrf": VRF.objects.first().rd,
+        cls.form_data = {
+            'vrf': vrf.pk,
+            'address': IPNetwork('192.0.2.99/24'),
+            'tenant': None,
+            'status': IPAddressStatusChoices.STATUS_RESERVED,
+            'role': IPAddressRoleChoices.ROLE_ANYCAST,
+            'interface': None,
+            'nat_inside': None,
+            'dns_name': 'example',
+            'description': 'A new IP address',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_ipaddress(self):
-
-        ipaddress = IPAddress.objects.first()
-        response = self.client.get(ipaddress.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_ipaddress_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "address,status",
             "address,status",
             "192.0.2.4/24,Active",
             "192.0.2.4/24,Active",
             "192.0.2.5/24,Active",
             "192.0.2.5/24,Active",
             "192.0.2.6/24,Active",
             "192.0.2.6/24,Active",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:ipaddress_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(IPAddress.objects.count(), 6)
 
 
+class VLANGroupTestCase(StandardTestCases.Views):
+    model = VLANGroup
 
 
-class VLANGroupTestCase(TestCase):
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vlangroup',
-                'ipam.add_vlangroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
 
 
         VLANGroup.objects.bulk_create([
         VLANGroup.objects.bulk_create([
             VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
             VLANGroup(name='VLAN Group 1', slug='vlan-group-1', site=site),
@@ -316,45 +220,29 @@ class VLANGroupTestCase(TestCase):
             VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
             VLANGroup(name='VLAN Group 3', slug='vlan-group-3', site=site),
         ])
         ])
 
 
-    def test_vlangroup_list(self):
-
-        url = reverse('ipam:vlangroup_list')
-        params = {
-            "site": Site.objects.first().slug,
+        cls.form_data = {
+            'name': 'VLAN Group X',
+            'slug': 'vlan-group-x',
+            'site': site.pk,
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlangroup_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "VLAN Group 4,vlan-group-4",
             "VLAN Group 4,vlan-group-4",
             "VLAN Group 5,vlan-group-5",
             "VLAN Group 5,vlan-group-5",
             "VLAN Group 6,vlan-group-6",
             "VLAN Group 6,vlan-group-6",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:vlangroup_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VLANGroup.objects.count(), 6)
+class VLANTestCase(StandardTestCases.Views):
+    model = VLAN
 
 
+    @classmethod
+    def setUpTestData(cls):
 
 
-class VLANTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'ipam.view_vlan',
-                'ipam.add_vlan',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        vlangroup = VLANGroup(name='VLAN Group 1', slug='vlan-group-1')
-        vlangroup.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        vlangroup = VLANGroup.objects.create(name='VLAN Group 1', slug='vlan-group-1', site=site)
+        role = Role.objects.create(name='Role 1', slug='role-1')
 
 
         VLAN.objects.bulk_create([
         VLAN.objects.bulk_create([
             VLAN(group=vlangroup, vid=101, name='VLAN101'),
             VLAN(group=vlangroup, vid=101, name='VLAN101'),
@@ -362,58 +250,43 @@ class VLANTestCase(TestCase):
             VLAN(group=vlangroup, vid=103, name='VLAN103'),
             VLAN(group=vlangroup, vid=103, name='VLAN103'),
         ])
         ])
 
 
-    def test_vlan_list(self):
-
-        url = reverse('ipam:vlan_list')
-        params = {
-            "group": VLANGroup.objects.first().slug,
+        cls.form_data = {
+            'site': site.pk,
+            'group': vlangroup.pk,
+            'vid': 999,
+            'name': 'VLAN999',
+            'tenant': None,
+            'status': VLANStatusChoices.STATUS_RESERVED,
+            'role': role.pk,
+            'description': 'A new VLAN',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlan(self):
-
-        vlan = VLAN.objects.first()
-        response = self.client.get(vlan.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_vlan_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "vid,name,status",
             "vid,name,status",
             "104,VLAN104,Active",
             "104,VLAN104,Active",
             "105,VLAN105,Active",
             "105,VLAN105,Active",
             "106,VLAN106,Active",
             "106,VLAN106,Active",
         )
         )
 
 
-        response = self.client.post(reverse('ipam:vlan_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VLAN.objects.count(), 6)
+class ServiceTestCase(StandardTestCases.Views):
+    model = Service
 
 
+    # Disable inapplicable tests
+    test_import_objects = None
 
 
-class ServiceTestCase(TestCase):
+    # TODO: Resolve URL for Service creation
+    test_create_object = None
 
 
-    def setUp(self):
-        user = create_test_user(permissions=['ipam.view_service'])
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
-
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
-
-        devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
-        devicetype.save()
-
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
-
-        device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        device.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
 
 
         Service.objects.bulk_create([
         Service.objects.bulk_create([
             Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101),
             Service(device=device, name='Service 1', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=101),
@@ -421,18 +294,13 @@ class ServiceTestCase(TestCase):
             Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103),
             Service(device=device, name='Service 3', protocol=ServiceProtocolChoices.PROTOCOL_TCP, port=103),
         ])
         ])
 
 
-    def test_service_list(self):
-
-        url = reverse('ipam:service_list')
-        params = {
-            "device_id": Device.objects.first(),
+        cls.form_data = {
+            'device': device.pk,
+            'virtual_machine': None,
+            'name': 'Service X',
+            'protocol': ServiceProtocolChoices.PROTOCOL_TCP,
+            'port': 999,
+            'ipaddresses': [],
+            'description': 'A new service',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
-
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_service(self):
-
-        service = Service.objects.first()
-        response = self.client.get(service.get_absolute_url())
-        self.assertEqual(response.status_code, 200)

+ 3 - 3
netbox/netbox/tests/test_views.py

@@ -1,6 +1,6 @@
 import urllib.parse
 import urllib.parse
 
 
-from django.test import TestCase
+from utilities.testing import TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 
 
@@ -11,7 +11,7 @@ class HomeViewTestCase(TestCase):
         url = reverse('home')
         url = reverse('home')
 
 
         response = self.client.get(url)
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
 
 
     def test_search(self):
     def test_search(self):
 
 
@@ -21,4 +21,4 @@ class HomeViewTestCase(TestCase):
         }
         }
 
 
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
         response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)

+ 46 - 73
netbox/secrets/tests/test_views.py

@@ -1,26 +1,22 @@
 import base64
 import base64
-import urllib.parse
 
 
-from django.test import Client, TestCase
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 from .constants import PRIVATE_KEY, PUBLIC_KEY
 from .constants import PRIVATE_KEY, PUBLIC_KEY
 
 
 
 
-class SecretRoleTestCase(TestCase):
+class SecretRoleTestCase(StandardTestCases.Views):
+    model = SecretRole
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'secrets.view_secretrole',
-                'secrets.add_secretrole',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
 
         SecretRole.objects.bulk_create([
         SecretRole.objects.bulk_create([
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
             SecretRole(name='Secret Role 1', slug='secret-role-1'),
@@ -28,65 +24,40 @@ class SecretRoleTestCase(TestCase):
             SecretRole(name='Secret Role 3', slug='secret-role-3'),
             SecretRole(name='Secret Role 3', slug='secret-role-3'),
         ])
         ])
 
 
-    def test_secretrole_list(self):
-
-        url = reverse('secrets:secretrole_list')
-
-        response = self.client.get(url, follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_secretrole_import(self):
+        cls.form_data = {
+            'name': 'Secret Role X',
+            'slug': 'secret-role-x',
+            'description': 'A secret role',
+            'users': [],
+            'groups': [],
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Secret Role 4,secret-role-4",
             "Secret Role 4,secret-role-4",
             "Secret Role 5,secret-role-5",
             "Secret Role 5,secret-role-5",
             "Secret Role 6,secret-role-6",
             "Secret Role 6,secret-role-6",
         )
         )
 
 
-        response = self.client.post(reverse('secrets:secretrole_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(SecretRole.objects.count(), 6)
+class SecretTestCase(StandardTestCases.Views):
+    model = Secret
 
 
+    # Disable inapplicable tests
+    test_create_object = None
 
 
-class SecretTestCase(TestCase):
+    # TODO: Check permissions enforcement on secrets.views.secret_edit
+    test_edit_object = None
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'secrets.view_secret',
-                'secrets.add_secret',
-            ]
-        )
+    @classmethod
+    def setUpTestData(cls):
 
 
-        # Set up a master key
-        userkey = UserKey(user=user, public_key=PUBLIC_KEY)
-        userkey.save()
-        master_key = userkey.get_master_key(PRIVATE_KEY)
-        self.session_key = SessionKey(userkey=userkey)
-        self.session_key.save(master_key)
-
-        self.client = Client()
-        self.client.force_login(user)
-
-        site = Site(name='Site 1', slug='site-1')
-        site.save()
-
-        manufacturer = Manufacturer(name='Manufacturer 1', slug='manufacturer-1')
-        manufacturer.save()
-
-        devicetype = DeviceType(manufacturer=manufacturer, model='Device Type 1')
-        devicetype.save()
-
-        devicerole = DeviceRole(name='Device Role 1', slug='device-role-1')
-        devicerole.save()
-
-        device = Device(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
-        device.save()
-
-        secretrole = SecretRole(name='Secret Role 1', slug='secret-role-1')
-        secretrole.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1')
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        device = Device.objects.create(name='Device 1', site=site, device_type=devicetype, device_role=devicerole)
+        secretrole = SecretRole.objects.create(name='Secret Role 1', slug='secret-role-1')
 
 
         Secret.objects.bulk_create([
         Secret.objects.bulk_create([
             Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
             Secret(device=device, role=secretrole, name='Secret 1', ciphertext=b'1234567890'),
@@ -94,23 +65,25 @@ class SecretTestCase(TestCase):
             Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
             Secret(device=device, role=secretrole, name='Secret 3', ciphertext=b'1234567890'),
         ])
         ])
 
 
-    def test_secret_list(self):
-
-        url = reverse('secrets:secret_list')
-        params = {
-            "role": SecretRole.objects.first().slug,
+        cls.form_data = {
+            'device': device.pk,
+            'role': secretrole.pk,
+            'name': 'Secret X',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
-        self.assertEqual(response.status_code, 200)
+    def setUp(self):
 
 
-    def test_secret(self):
+        super().setUp()
 
 
-        secret = Secret.objects.first()
-        response = self.client.get(secret.get_absolute_url(), follow=True)
-        self.assertEqual(response.status_code, 200)
+        # Set up a master key for the test user
+        userkey = UserKey(user=self.user, public_key=PUBLIC_KEY)
+        userkey.save()
+        master_key = userkey.get_master_key(PRIVATE_KEY)
+        self.session_key = SessionKey(userkey=userkey)
+        self.session_key.save(master_key)
 
 
-    def test_secret_import(self):
+    def test_import_objects(self):
+        self.add_permissions('secrets.add_secret')
 
 
         csv_data = (
         csv_data = (
             "device,role,name,plaintext",
             "device,role,name,plaintext",
@@ -125,5 +98,5 @@ class SecretTestCase(TestCase):
 
 
         response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)})
         response = self.client.post(reverse('secrets:secret_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
+        self.assertHttpStatus(response, 200)
         self.assertEqual(Secret.objects.count(), 6)
         self.assertEqual(Secret.objects.count(), 6)

+ 27 - 64
netbox/tenancy/tests/test_views.py

@@ -1,23 +1,16 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
-
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
-from utilities.testing import create_test_user
+from utilities.testing import StandardTestCases
 
 
 
 
-class TenantGroupTestCase(TestCase):
+class TenantGroupTestCase(StandardTestCases.Views):
+    model = TenantGroup
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'tenancy.view_tenantgroup',
-                'tenancy.add_tenantgroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
 
         TenantGroup.objects.bulk_create([
         TenantGroup.objects.bulk_create([
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
             TenantGroup(name='Tenant Group 1', slug='tenant-group-1'),
@@ -25,42 +18,26 @@ class TenantGroupTestCase(TestCase):
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
             TenantGroup(name='Tenant Group 3', slug='tenant-group-3'),
         ])
         ])
 
 
-    def test_tenantgroup_list(self):
-
-        url = reverse('tenancy:tenantgroup_list')
-
-        response = self.client.get(url, follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenantgroup_import(self):
+        cls.form_data = {
+            'name': 'Tenant Group X',
+            'slug': 'tenant-group-x',
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Tenant Group 4,tenant-group-4",
             "Tenant Group 4,tenant-group-4",
             "Tenant Group 5,tenant-group-5",
             "Tenant Group 5,tenant-group-5",
             "Tenant Group 6,tenant-group-6",
             "Tenant Group 6,tenant-group-6",
         )
         )
 
 
-        response = self.client.post(reverse('tenancy:tenantgroup_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(TenantGroup.objects.count(), 6)
 
 
+class TenantTestCase(StandardTestCases.Views):
+    model = Tenant
 
 
-class TenantTestCase(TestCase):
+    @classmethod
+    def setUpTestData(cls):
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'tenancy.view_tenant',
-                'tenancy.add_tenant',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
-
-        tenantgroup = TenantGroup(name='Tenant Group 1', slug='tenant-group-1')
-        tenantgroup.save()
+        tenantgroup = TenantGroup.objects.create(name='Tenant Group 1', slug='tenant-group-1')
 
 
         Tenant.objects.bulk_create([
         Tenant.objects.bulk_create([
             Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup),
             Tenant(name='Tenant 1', slug='tenant-1', group=tenantgroup),
@@ -68,32 +45,18 @@ class TenantTestCase(TestCase):
             Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup),
             Tenant(name='Tenant 3', slug='tenant-3', group=tenantgroup),
         ])
         ])
 
 
-    def test_tenant_list(self):
-
-        url = reverse('tenancy:tenant_list')
-        params = {
-            "group": TenantGroup.objects.first().slug,
+        cls.form_data = {
+            'name': 'Tenant X',
+            'slug': 'tenant-x',
+            'group': tenantgroup.pk,
+            'description': 'A new tenant',
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)), follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenant(self):
-
-        tenant = Tenant.objects.first()
-        response = self.client.get(tenant.get_absolute_url(), follow=True)
-        self.assertEqual(response.status_code, 200)
-
-    def test_tenant_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Tenant 4,tenant-4",
             "Tenant 4,tenant-4",
             "Tenant 5,tenant-5",
             "Tenant 5,tenant-5",
             "Tenant 6,tenant-6",
             "Tenant 6,tenant-6",
         )
         )
-
-        response = self.client.post(reverse('tenancy:tenant_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Tenant.objects.count(), 6)

+ 0 - 79
netbox/utilities/testing.py

@@ -1,79 +0,0 @@
-import logging
-from contextlib import contextmanager
-
-from django.contrib.auth.models import Permission, User
-from rest_framework.test import APITestCase as _APITestCase
-
-from users.models import Token
-
-
-class APITestCase(_APITestCase):
-
-    def setUp(self):
-        """
-        Create a superuser and token for API calls.
-        """
-        self.user = User.objects.create(username='testuser', is_superuser=True)
-        self.token = Token.objects.create(user=self.user)
-        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
-
-    def assertHttpStatus(self, response, expected_status):
-        """
-        Provide more detail in the event of an unexpected HTTP response.
-        """
-        err_message = "Expected HTTP status {}; received {}: {}"
-        self.assertEqual(response.status_code, expected_status, err_message.format(
-            expected_status, response.status_code, getattr(response, 'data', 'No data')
-        ))
-
-
-def create_test_user(username='testuser', permissions=list()):
-    """
-    Create a User with the given permissions.
-    """
-    user = User.objects.create_user(username=username)
-    for perm_name in permissions:
-        app, codename = perm_name.split('.')
-        perm = Permission.objects.get(content_type__app_label=app, codename=codename)
-        user.user_permissions.add(perm)
-
-    return user
-
-
-def choices_to_dict(choices_list):
-    """
-    Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example:
-
-        [
-            {
-                "value": "choice-1",
-                "label": "First Choice"
-            },
-            {
-                "value": "choice-2",
-                "label": "Second Choice"
-            }
-        ]
-
-    Becomes:
-
-        {
-            "choice-1": "First Choice",
-            "choice-2": "Second Choice
-        }
-    """
-    return {
-        choice['value']: choice['label'] for choice in choices_list
-    }
-
-
-@contextmanager
-def disable_warnings(logger_name):
-    """
-    Temporarily suppress expected warning messages to keep the test output clean.
-    """
-    logger = logging.getLogger(logger_name)
-    current_level = logger.level
-    logger.setLevel(logging.ERROR)
-    yield
-    logger.setLevel(current_level)

+ 2 - 0
netbox/utilities/testing/__init__.py

@@ -0,0 +1,2 @@
+from .testcases import *
+from .utils import *

+ 249 - 0
netbox/utilities/testing/testcases.py

@@ -0,0 +1,249 @@
+from django.contrib.auth.models import Permission, User
+from django.core.exceptions import ObjectDoesNotExist
+from django.test import Client, TestCase as _TestCase, override_settings
+from django.urls import reverse, NoReverseMatch
+from rest_framework.test import APIClient
+
+from users.models import Token
+from .utils import disable_warnings, model_to_dict, post_data
+
+
+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 <app>.<action>_<model>.
+        """
+        for name in names:
+            app, codename = name.split('.')
+            perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+            self.user.user_permissions.add(perm)
+
+    def remove_permissions(self, *names):
+        """
+        Remove a set of permissions from the test user, if assigned.
+        """
+        for name in names:
+            app, codename = name.split('.')
+            perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+            self.user.user_permissions.remove(perm)
+
+    #
+    # Convenience methods
+    #
+
+    def assertHttpStatus(self, response, expected_status):
+        """
+        TestCase method. Provide more detail in the event of an unexpected HTTP response.
+        """
+        err_message = "Expected HTTP status {}; received {}: {}"
+        self.assertEqual(response.status_code, expected_status, err_message.format(
+            expected_status, response.status_code, getattr(response, 'data', 'No data')
+        ))
+
+
+class APITestCase(TestCase):
+    client_class = APIClient
+
+    def setUp(self):
+        """
+        Create a superuser and token for API calls.
+        """
+        self.user = User.objects.create(username='testuser', is_superuser=True)
+        self.token = Token.objects.create(user=self.user)
+        self.header = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key)}
+
+
+class StandardTestCases:
+    """
+    We keep any TestCases with test_* methods inside a class to prevent unittest from trying to run them.
+    """
+
+    class Views(TestCase):
+        """
+        Stock TestCase suitable for testing all standard View functions:
+            - List objects
+            - View single object
+            - Create new object
+            - Modify existing object
+            - Delete existing object
+            - Import multiple new objects
+        """
+        model = None
+        form_data = {}
+        csv_data = {}
+
+        maxDiff = None
+
+        def __init__(self, *args, **kwargs):
+
+            super().__init__(*args, **kwargs)
+
+            if self.model is None:
+                raise Exception("Test case requires model to be defined")
+
+        def _get_url(self, action, instance=None):
+            """
+            Return the URL name for a specific action. An instance must be specified for
+            get/edit/delete views.
+            """
+            url_format = '{}:{}_{{}}'.format(
+                self.model._meta.app_label,
+                self.model._meta.model_name
+            )
+
+            if action in ('list', 'add', 'import'):
+                return reverse(url_format.format(action))
+
+            elif action in ('get', 'edit', 'delete'):
+                if instance is None:
+                    raise Exception("Resolving {} URL requires specifying an instance".format(action))
+                # Attempt to resolve using slug first
+                if hasattr(self.model, 'slug'):
+                    try:
+                        return reverse(url_format.format(action), kwargs={'slug': instance.slug})
+                    except NoReverseMatch:
+                        pass
+                return reverse(url_format.format(action), kwargs={'pk': instance.pk})
+
+            else:
+                raise Exception("Invalid action for URL resolution: {}".format(action))
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_list_objects(self):
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.get(self._get_url('list')), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.get(self._get_url('list'))
+            self.assertHttpStatus(response, 200)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_get_object(self):
+            instance = self.model.objects.first()
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.get(instance.get_absolute_url())
+            self.assertHttpStatus(response, 200)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_create_object(self):
+            initial_count = self.model.objects.count()
+            request = {
+                'path': self._get_url('add'),
+                'data': post_data(self.form_data),
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            self.assertEqual(initial_count + 1, self.model.objects.count())
+            instance = self.model.objects.order_by('-pk').first()
+            self.assertDictEqual(model_to_dict(instance), self.form_data)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_edit_object(self):
+            instance = self.model.objects.first()
+
+            request = {
+                'path': self._get_url('edit', instance),
+                'data': post_data(self.form_data),
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.change_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            instance = self.model.objects.get(pk=instance.pk)
+            self.assertDictEqual(model_to_dict(instance), self.form_data)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_delete_object(self):
+            instance = self.model.objects.first()
+
+            request = {
+                'path': self._get_url('delete', instance),
+                'data': {'confirm': True},
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.delete_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            with self.assertRaises(ObjectDoesNotExist):
+                self.model.objects.get(pk=instance.pk)
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def test_import_objects(self):
+            initial_count = self.model.objects.count()
+            request = {
+                'path': self._get_url('import'),
+                'data': {
+                    'csv': '\n'.join(self.csv_data)
+                }
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.view_{}'.format(self.model._meta.app_label, self.model._meta.model_name),
+                '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 200)
+
+            self.assertEqual(self.model.objects.count(), initial_count + len(self.csv_data) - 1)

+ 102 - 0
netbox/utilities/testing/utils.py

@@ -0,0 +1,102 @@
+import logging
+from contextlib import contextmanager
+
+from django.contrib.auth.models import Permission, User
+from django.forms.models import model_to_dict as _model_to_dict
+
+
+def model_to_dict(instance, fields=None, exclude=None):
+    """
+    Customized wrapper for Django's built-in model_to_dict(). Does the following:
+      - Excludes the instance ID field
+      - Exclude any fields prepended with an underscore
+      - Convert any assigned tags to a comma-separated string
+    """
+    _exclude = ['id']
+    if exclude is not None:
+        _exclude += exclude
+
+    model_dict = _model_to_dict(instance, fields=fields, exclude=_exclude)
+
+    for key in list(model_dict.keys()):
+        if key.startswith('_'):
+            del model_dict[key]
+
+        # TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
+        elif key == 'tags':
+            model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']]))
+
+        # Convert ManyToManyField to list of instance PKs
+        elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'):
+            model_dict[key] = [obj.pk for obj in model_dict[key]]
+
+    return model_dict
+
+
+def post_data(data):
+    """
+    Take a dictionary of test data (suitable for comparison to an instance) and return a dict suitable for POSTing.
+    """
+    ret = {}
+
+    for key, value in data.items():
+        if value is None:
+            ret[key] = ''
+        elif type(value) in (list, tuple):
+            ret[key] = value
+        else:
+            ret[key] = str(value)
+
+    return ret
+
+
+def create_test_user(username='testuser', permissions=list()):
+    """
+    Create a User with the given permissions.
+    """
+    user = User.objects.create_user(username=username)
+    for perm_name in permissions:
+        app, codename = perm_name.split('.')
+        perm = Permission.objects.get(content_type__app_label=app, codename=codename)
+        user.user_permissions.add(perm)
+
+    return user
+
+
+def choices_to_dict(choices_list):
+    """
+    Convert a list of field choices to a dictionary suitable for direct comparison with a ChoiceSet. For example:
+
+        [
+            {
+                "value": "choice-1",
+                "label": "First Choice"
+            },
+            {
+                "value": "choice-2",
+                "label": "Second Choice"
+            }
+        ]
+
+    Becomes:
+
+        {
+            "choice-1": "First Choice",
+            "choice-2": "Second Choice
+        }
+    """
+    return {
+        choice['value']: choice['label'] for choice in choices_list
+    }
+
+
+@contextmanager
+def disable_warnings(logger_name):
+    """
+    Temporarily suppress expected warning messages to keep the test output clean.
+    """
+    logger = logging.getLogger(logger_name)
+    current_level = logger.level
+    logger.setLevel(logging.ERROR)
+    yield
+    logger.setLevel(current_level)

+ 68 - 128
netbox/virtualization/tests/test_views.py

@@ -1,23 +1,18 @@
-import urllib.parse
-
-from django.test import Client, TestCase
-from django.urls import reverse
-
-from utilities.testing import create_test_user
+from dcim.models import DeviceRole, Platform, Site
+from utilities.testing import StandardTestCases
+from virtualization.choices import *
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
 
 
 
 
-class ClusterGroupTestCase(TestCase):
+class ClusterGroupTestCase(StandardTestCases.Views):
+    model = ClusterGroup
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_clustergroup',
-                'virtualization.add_clustergroup',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
+
+    @classmethod
+    def setUpTestData(cls):
 
 
         ClusterGroup.objects.bulk_create([
         ClusterGroup.objects.bulk_create([
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
@@ -25,39 +20,28 @@ class ClusterGroupTestCase(TestCase):
             ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
             ClusterGroup(name='Cluster Group 3', slug='cluster-group-3'),
         ])
         ])
 
 
-    def test_clustergroup_list(self):
-
-        url = reverse('virtualization:clustergroup_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_clustergroup_import(self):
+        cls.form_data = {
+            'name': 'Cluster Group X',
+            'slug': 'cluster-group-x',
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Cluster Group 4,cluster-group-4",
             "Cluster Group 4,cluster-group-4",
             "Cluster Group 5,cluster-group-5",
             "Cluster Group 5,cluster-group-5",
             "Cluster Group 6,cluster-group-6",
             "Cluster Group 6,cluster-group-6",
         )
         )
 
 
-        response = self.client.post(reverse('virtualization:clustergroup_import'), {'csv': '\n'.join(csv_data)})
 
 
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(ClusterGroup.objects.count(), 6)
+class ClusterTypeTestCase(StandardTestCases.Views):
+    model = ClusterType
 
 
+    # Disable inapplicable tests
+    test_get_object = None
+    test_delete_object = None
 
 
-class ClusterTypeTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_clustertype',
-                'virtualization.add_clustertype',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
         ClusterType.objects.bulk_create([
         ClusterType.objects.bulk_create([
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
             ClusterType(name='Cluster Type 1', slug='cluster-type-1'),
@@ -65,45 +49,28 @@ class ClusterTypeTestCase(TestCase):
             ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
             ClusterType(name='Cluster Type 3', slug='cluster-type-3'),
         ])
         ])
 
 
-    def test_clustertype_list(self):
-
-        url = reverse('virtualization:clustertype_list')
-
-        response = self.client.get(url)
-        self.assertEqual(response.status_code, 200)
-
-    def test_clustertype_import(self):
+        cls.form_data = {
+            'name': 'Cluster Type X',
+            'slug': 'cluster-type-x',
+        }
 
 
-        csv_data = (
+        cls.csv_data = (
             "name,slug",
             "name,slug",
             "Cluster Type 4,cluster-type-4",
             "Cluster Type 4,cluster-type-4",
             "Cluster Type 5,cluster-type-5",
             "Cluster Type 5,cluster-type-5",
             "Cluster Type 6,cluster-type-6",
             "Cluster Type 6,cluster-type-6",
         )
         )
 
 
-        response = self.client.post(reverse('virtualization:clustertype_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(ClusterType.objects.count(), 6)
-
-
-class ClusterTestCase(TestCase):
 
 
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_cluster',
-                'virtualization.add_cluster',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+class ClusterTestCase(StandardTestCases.Views):
+    model = Cluster
 
 
-        clustergroup = ClusterGroup(name='Cluster Group 1', slug='cluster-group-1')
-        clustergroup.save()
+    @classmethod
+    def setUpTestData(cls):
 
 
-        clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
-        clustertype.save()
+        site = Site.objects.create(name='Site 1', slug='site-1')
+        clustergroup = ClusterGroup.objects.create(name='Cluster Group 1', slug='cluster-group-1')
+        clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
 
 
         Cluster.objects.bulk_create([
         Cluster.objects.bulk_create([
             Cluster(name='Cluster 1', group=clustergroup, type=clustertype),
             Cluster(name='Cluster 1', group=clustergroup, type=clustertype),
@@ -111,55 +78,34 @@ class ClusterTestCase(TestCase):
             Cluster(name='Cluster 3', group=clustergroup, type=clustertype),
             Cluster(name='Cluster 3', group=clustergroup, type=clustertype),
         ])
         ])
 
 
-    def test_cluster_list(self):
-
-        url = reverse('virtualization:cluster_list')
-        params = {
-            "group": ClusterGroup.objects.first().slug,
-            "type": ClusterType.objects.first().slug,
+        cls.form_data = {
+            'name': 'Cluster X',
+            'group': clustergroup.pk,
+            'type': clustertype.pk,
+            'tenant': None,
+            'site': site.pk,
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_cluster(self):
-
-        cluster = Cluster.objects.first()
-        response = self.client.get(cluster.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_cluster_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,type",
             "name,type",
             "Cluster 4,Cluster Type 1",
             "Cluster 4,Cluster Type 1",
             "Cluster 5,Cluster Type 1",
             "Cluster 5,Cluster Type 1",
             "Cluster 6,Cluster Type 1",
             "Cluster 6,Cluster Type 1",
         )
         )
 
 
-        response = self.client.post(reverse('virtualization:cluster_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(Cluster.objects.count(), 6)
 
 
+class VirtualMachineTestCase(StandardTestCases.Views):
+    model = VirtualMachine
 
 
-class VirtualMachineTestCase(TestCase):
-
-    def setUp(self):
-        user = create_test_user(
-            permissions=[
-                'virtualization.view_virtualmachine',
-                'virtualization.add_virtualmachine',
-            ]
-        )
-        self.client = Client()
-        self.client.force_login(user)
+    @classmethod
+    def setUpTestData(cls):
 
 
-        clustertype = ClusterType(name='Cluster Type 1', slug='cluster-type-1')
-        clustertype.save()
-
-        cluster = Cluster(name='Cluster 1', type=clustertype)
-        cluster.save()
+        devicerole = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
+        platform = Platform.objects.create(name='Platform 1', slug='platform-1')
+        clustertype = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
+        cluster = Cluster.objects.create(name='Cluster 1', type=clustertype)
 
 
         VirtualMachine.objects.bulk_create([
         VirtualMachine.objects.bulk_create([
             VirtualMachine(name='Virtual Machine 1', cluster=cluster),
             VirtualMachine(name='Virtual Machine 1', cluster=cluster),
@@ -167,32 +113,26 @@ class VirtualMachineTestCase(TestCase):
             VirtualMachine(name='Virtual Machine 3', cluster=cluster),
             VirtualMachine(name='Virtual Machine 3', cluster=cluster),
         ])
         ])
 
 
-    def test_virtualmachine_list(self):
-
-        url = reverse('virtualization:virtualmachine_list')
-        params = {
-            "cluster_id": Cluster.objects.first().pk,
+        cls.form_data = {
+            'cluster': cluster.pk,
+            'tenant': None,
+            'platform': None,
+            'name': 'Virtual Machine X',
+            'status': VirtualMachineStatusChoices.STATUS_STAGED,
+            'role': devicerole.pk,
+            'primary_ip4': None,
+            'primary_ip6': None,
+            'vcpus': 4,
+            'memory': 32768,
+            'disk': 4000,
+            'comments': 'Some comments',
+            'tags': 'Alpha,Bravo,Charlie',
+            'local_context_data': None,
         }
         }
 
 
-        response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
-        self.assertEqual(response.status_code, 200)
-
-    def test_virtualmachine(self):
-
-        virtualmachine = VirtualMachine.objects.first()
-        response = self.client.get(virtualmachine.get_absolute_url())
-        self.assertEqual(response.status_code, 200)
-
-    def test_virtualmachine_import(self):
-
-        csv_data = (
+        cls.csv_data = (
             "name,cluster",
             "name,cluster",
             "Virtual Machine 4,Cluster 1",
             "Virtual Machine 4,Cluster 1",
             "Virtual Machine 5,Cluster 1",
             "Virtual Machine 5,Cluster 1",
             "Virtual Machine 6,Cluster 1",
             "Virtual Machine 6,Cluster 1",
         )
         )
-
-        response = self.client.post(reverse('virtualization:virtualmachine_import'), {'csv': '\n'.join(csv_data)})
-
-        self.assertEqual(response.status_code, 200)
-        self.assertEqual(VirtualMachine.objects.count(), 6)

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно