test_forms.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. from django.contrib.contenttypes.models import ContentType
  2. from django.test import TestCase
  3. from dcim.constants import InterfaceTypeChoices
  4. from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
  5. from ipam.forms import PrefixForm, VLANIDBulkCreateForm
  6. from ipam.forms.bulk_import import IPAddressImportForm
  7. class PrefixFormTestCase(TestCase):
  8. default_dynamic_params = '[{"fieldName":"scope","queryParam":"available_at_site"}]'
  9. @classmethod
  10. def setUpTestData(cls):
  11. cls.site = Site.objects.create(name='Site 1', slug='site-1')
  12. def test_vlan_field_sets_dynamic_params_by_default(self):
  13. """data-dynamic-params present when no scope_type selected"""
  14. form = PrefixForm(data={})
  15. assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == self.default_dynamic_params
  16. def test_vlan_field_sets_dynamic_params_for_scope_site(self):
  17. """data-dynamic-params present when scope type is Site and when scope is specifc site"""
  18. form = PrefixForm(data={
  19. 'scope_type': ContentType.objects.get_for_model(Site).id,
  20. 'scope': self.site,
  21. })
  22. assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == self.default_dynamic_params
  23. def test_vlan_field_sets_dynamic_params_for_scope_site_group(self):
  24. """data-dynamic-params present with available_at_site_group when scope type is Site Group"""
  25. site_group = SiteGroup.objects.create(name='Site Group 1', slug='site-group-1')
  26. form = PrefixForm(data={
  27. 'scope_type': ContentType.objects.get_for_model(SiteGroup).id,
  28. 'scope': site_group,
  29. })
  30. expected = '[{"fieldName":"scope","queryParam":"available_at_site_group"}]'
  31. assert form.fields['vlan'].widget.attrs['data-dynamic-params'] == expected
  32. def test_vlan_field_does_not_set_dynamic_params_for_other_scopes(self):
  33. """data-dynamic-params not present when scope type is not Site or Site Group"""
  34. cases = [
  35. Region(name='Region 1', slug='region-1'),
  36. Location(site=self.site, name='Location 1', slug='location-1'),
  37. ]
  38. for case in cases:
  39. form = PrefixForm(data={
  40. 'scope_type': ContentType.objects.get_for_model(case._meta.model).id,
  41. 'scope': case,
  42. })
  43. assert 'data-dynamic-params' not in form.fields['vlan'].widget.attrs
  44. class IPAddressImportFormTestCase(TestCase):
  45. """Tests for IPAddressImportForm bulk import behavior."""
  46. @classmethod
  47. def setUpTestData(cls):
  48. site = Site.objects.create(name='Site 1', slug='site-1')
  49. manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
  50. device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
  51. device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
  52. cls.device = Device.objects.create(
  53. name='Device 1',
  54. site=site,
  55. device_type=device_type,
  56. role=device_role,
  57. )
  58. cls.interface = Interface.objects.create(
  59. device=cls.device,
  60. name='eth0',
  61. type=InterfaceTypeChoices.TYPE_1GE_FIXED,
  62. )
  63. def test_import_with_empty_is_primary_column_no_device(self):
  64. """
  65. Regression test for #22561: importing an IP where the is_primary/is_oob columns are
  66. present but empty (and no device/VM specified) should succeed, not raise AttributeError.
  67. """
  68. form = IPAddressImportForm(data={
  69. 'address': '172.16.0.1/20',
  70. 'status': 'active',
  71. 'device': '',
  72. 'virtual_machine': '',
  73. 'interface': '',
  74. 'is_primary': '',
  75. 'is_oob': '',
  76. 'description': 'gateway for group A - Site 01',
  77. })
  78. self.assertTrue(form.is_valid(), form.errors)
  79. ip = form.save()
  80. self.assertEqual(str(ip.address), '172.16.0.1/20')
  81. def test_import_with_false_is_primary_no_device(self):
  82. """
  83. Regression test for #22561: importing an IP with an explicit is_primary=false (and no
  84. device/VM specified) should succeed as a no-op, not raise AttributeError. An explicit
  85. falsy boolean is not caught by clean_is_primary()'s "column absent" check.
  86. """
  87. form = IPAddressImportForm(data={
  88. 'address': '172.16.0.1/20',
  89. 'status': 'active',
  90. 'is_primary': 'false',
  91. 'is_oob': 'false',
  92. 'description': 'no parent specified',
  93. })
  94. self.assertTrue(form.is_valid(), form.errors)
  95. ip = form.save()
  96. self.assertEqual(str(ip.address), '172.16.0.1/20')
  97. def test_primary_not_cleared_by_subsequent_non_primary_row_with_device(self):
  98. """
  99. Guard against re-breaking #21440 while fixing #22561: importing a second IP with
  100. is_primary=false (device specified) must not clear the primary IP set by a previous
  101. row. The save-side parent guard must leave the conservative "only clear if currently
  102. primary" behavior intact.
  103. """
  104. form1 = IPAddressImportForm(data={
  105. 'address': '10.10.10.1/24',
  106. 'status': 'active',
  107. 'device': 'Device 1',
  108. 'interface': 'eth0',
  109. 'is_primary': True,
  110. })
  111. self.assertTrue(form1.is_valid(), form1.errors)
  112. ip1 = form1.save()
  113. self.device.refresh_from_db()
  114. self.assertEqual(self.device.primary_ip4, ip1)
  115. form2 = IPAddressImportForm(data={
  116. 'address': '10.10.10.2/24',
  117. 'status': 'active',
  118. 'device': 'Device 1',
  119. 'interface': 'eth0',
  120. 'is_primary': False,
  121. })
  122. self.assertTrue(form2.is_valid(), form2.errors)
  123. form2.save()
  124. self.device.refresh_from_db()
  125. self.assertEqual(
  126. self.device.primary_ip4, ip1, "primary IP was incorrectly cleared by a row with is_primary=False"
  127. )
  128. def test_oob_import_not_cleared_by_subsequent_non_oob_row(self):
  129. """
  130. Regression test for #21440: importing a second IP with is_oob=False should
  131. not clear the OOB IP set by a previous row with is_oob=True.
  132. """
  133. form1 = IPAddressImportForm(data={
  134. 'address': '10.10.10.1/24',
  135. 'status': 'active',
  136. 'device': 'Device 1',
  137. 'interface': 'eth0',
  138. 'is_oob': True,
  139. })
  140. self.assertTrue(form1.is_valid(), form1.errors)
  141. ip1 = form1.save()
  142. self.device.refresh_from_db()
  143. self.assertEqual(self.device.oob_ip, ip1)
  144. form2 = IPAddressImportForm(data={
  145. 'address': '2001:db8::1/64',
  146. 'status': 'active',
  147. 'device': 'Device 1',
  148. 'interface': 'eth0',
  149. 'is_oob': False,
  150. })
  151. self.assertTrue(form2.is_valid(), form2.errors)
  152. form2.save()
  153. self.device.refresh_from_db()
  154. self.assertEqual(self.device.oob_ip, ip1, "OOB IP was incorrectly cleared by a row with is_oob=False")
  155. class VLANFormTestCase(TestCase):
  156. def test_bulk_create_valid_patterns(self):
  157. """Single values, ranges, and combinations expand to sorted, deduplicated VLAN IDs."""
  158. cases = (
  159. ('100', [100]),
  160. ('5,10,20', [5, 10, 20]),
  161. ('10-20', list(range(10, 21))),
  162. ('1,10-20,300-305', [1, *range(10, 21), *range(300, 306)]),
  163. (' 5 , 7 - 9 ', [5, 7, 8, 9]),
  164. ('5,5,4-6', [4, 5, 6]),
  165. )
  166. for pattern, expected in cases:
  167. with self.subTest(pattern=pattern):
  168. form = VLANIDBulkCreateForm({'pattern': pattern})
  169. self.assertTrue(form.is_valid(), form.errors)
  170. self.assertEqual(form.cleaned_data['pattern'], expected)
  171. def test_bulk_create_invalid_patterns(self):
  172. """Malformed, descending, or out-of-range patterns are rejected with an error on the pattern field."""
  173. cases = ('', 'abc', '10,abc', '20-10', '10-', '5,', '-5', '0', '4095')
  174. for pattern in cases:
  175. with self.subTest(pattern=pattern):
  176. form = VLANIDBulkCreateForm({'pattern': pattern})
  177. self.assertFalse(form.is_valid())
  178. self.assertIn('pattern', form.errors)