|
|
@@ -234,12 +234,7 @@ class ModuleTypeFormTestCase(TestCase):
|
|
|
class ModuleBayTemplateImportFormTestCase(TestCase):
|
|
|
|
|
|
def test_module_bay_types_prefers_manufacturer_specific_match_over_global(self):
|
|
|
- """
|
|
|
- ModuleBayType's unique constraint is on (manufacturer, name), not name alone, so a
|
|
|
- global type and a manufacturer-scoped type can legally share the same name. Referencing
|
|
|
- that name by import should resolve to the manufacturer-specific match only, not attach
|
|
|
- both.
|
|
|
- """
|
|
|
+ """A name shared by a global and a manufacturer-scoped type resolves to the scoped one."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
global_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28-global')
|
|
|
scoped_type = ModuleBayType.objects.create(
|
|
|
@@ -280,10 +275,7 @@ class ModuleBayTemplateImportFormTestCase(TestCase):
|
|
|
)
|
|
|
|
|
|
def test_module_bay_types_prefers_manufacturer_specific_match_over_global_for_module_type(self):
|
|
|
- """
|
|
|
- Same disambiguation as the device_type-scoped case, but through the module_type path
|
|
|
- (a module bay template nested within a ModuleType rather than a DeviceType).
|
|
|
- """
|
|
|
+ """Same disambiguation, but for a module bay template nested under a ModuleType."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
global_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28-global')
|
|
|
scoped_type = ModuleBayType.objects.create(
|
|
|
@@ -334,11 +326,7 @@ class ModuleBayTemplateImportFormTestCase(TestCase):
|
|
|
self.assertFalse(form.save().enabled)
|
|
|
|
|
|
def test_import_export_round_trip_preserves_module_bay_types(self):
|
|
|
- """
|
|
|
- A ModuleBayTemplate exported via to_yaml() and re-imported through this form should
|
|
|
- end up with the same module bay types, closing the exact export/import loop this
|
|
|
- feature exists for.
|
|
|
- """
|
|
|
+ """to_yaml() then re-import through this form preserves module bay types."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
bay_type_a = ModuleBayType.objects.create(name='SFP28', slug='sfp28')
|
|
|
bay_type_b = ModuleBayType.objects.create(name='QSFP28', slug='qsfp28')
|
|
|
@@ -363,13 +351,7 @@ class ModuleBayTemplateImportFormTestCase(TestCase):
|
|
|
)
|
|
|
|
|
|
def test_module_bay_types_permits_a_different_manufacturers_type(self):
|
|
|
- """
|
|
|
- The UI (ModuleBayTemplateForm) and REST API place no manufacturer restriction on
|
|
|
- module_bay_types -- a third-party device may legitimately declare a bay compatible
|
|
|
- with another manufacturer's proprietary bay type. Import must permit the same, and a
|
|
|
- type assigned this way must survive an export/re-import round trip rather than
|
|
|
- becoming permanently unimportable.
|
|
|
- """
|
|
|
+ """The UI/API place no manufacturer restriction on module_bay_types; import must match."""
|
|
|
juniper = Manufacturer.objects.create(name='Juniper', slug='juniper')
|
|
|
cisco = Manufacturer.objects.create(name='Cisco', slug='cisco')
|
|
|
cisco_bay_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28-cisco', manufacturer=cisco)
|
|
|
@@ -412,15 +394,14 @@ class ModuleBayTemplateImportFormTestCase(TestCase):
|
|
|
'module_bay_types': ['SFP28'],
|
|
|
})
|
|
|
self.assertFalse(form.is_valid())
|
|
|
- self.assertIn('module_bay_types', form.errors)
|
|
|
+ # Must name both manufacturers, not just error -- a plain invalid_choice would also
|
|
|
+ # pass assertIn() and mask a regression of the scoping removed in 6f3c537.
|
|
|
+ errors = form.errors.get('module_bay_types', [])
|
|
|
+ self.assertTrue(any('Cisco' in e and 'Arista' in e for e in errors), errors)
|
|
|
|
|
|
|
|
|
class ModuleBayImportFormTestCase(TestCase):
|
|
|
- """
|
|
|
- ModuleBayImportForm covers real ModuleBay instances created directly via CSV (as
|
|
|
- opposed to ModuleBayTemplateImportForm, which covers templates nested under a device or
|
|
|
- module type's YAML definition) -- the same class of round-trip gap, on the instance side.
|
|
|
- """
|
|
|
+ """Covers real ModuleBay instances via CSV, as opposed to templates via ModuleBayTemplateImportForm."""
|
|
|
|
|
|
def test_module_bay_types_csv_import(self):
|
|
|
device = create_test_device('Module Bay Import Device')
|
|
|
@@ -463,7 +444,10 @@ class ModuleBayImportFormTestCase(TestCase):
|
|
|
'module_bay_types': 'SFP28',
|
|
|
})
|
|
|
self.assertFalse(form.is_valid())
|
|
|
- self.assertIn('module_bay_types', form.errors)
|
|
|
+ # Must name both manufacturers, not just error -- a plain invalid_choice would also
|
|
|
+ # pass assertIn() and mask a regression of the scoping removed in 6f3c537.
|
|
|
+ errors = form.errors.get('module_bay_types', [])
|
|
|
+ self.assertTrue(any('Cisco' in e and 'Arista' in e for e in errors), errors)
|
|
|
|
|
|
|
|
|
class ModuleTypeImportFormTestCase(TestCase):
|
|
|
@@ -502,11 +486,7 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
self.assertNotIn(global_type, module_type.module_bay_types.all())
|
|
|
|
|
|
def test_module_bay_types_accepts_csv_comma_separated_string(self):
|
|
|
- """
|
|
|
- module_bay_types is a CSVModelMultipleChoiceField because ModuleTypeImportForm also
|
|
|
- serves plain CSV bulk import, where the cell value arrives as a comma-separated
|
|
|
- string rather than a list.
|
|
|
- """
|
|
|
+ """This form also serves plain CSV import, where the value is a string, not a list."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
bay_type_a = ModuleBayType.objects.create(name='SFP28', slug='sfp28')
|
|
|
bay_type_b = ModuleBayType.objects.create(name='QSFP28', slug='qsfp28')
|
|
|
@@ -536,15 +516,10 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
self.assertFalse(form.save().module_bay_types.exists())
|
|
|
|
|
|
def test_module_bay_types_round_trips_through_the_table_column_export_value(self):
|
|
|
- """
|
|
|
- ManyToManyColumn's export separator is ", " (comma + space, django-tables2's
|
|
|
- default), not the bare "," this field's CSVModelMultipleChoiceField splits on -- so
|
|
|
- re-importing NetBox's own CSV export of a multi-value module_bay_types column must
|
|
|
- not fail on the leading space of every value after the first.
|
|
|
- """
|
|
|
+ """The table's CSV export (multi-value separator, name-only transform) must be re-importable."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
- bay_type_a = ModuleBayType.objects.create(name='SFP28', slug='sfp28')
|
|
|
- bay_type_b = ModuleBayType.objects.create(name='QSFP28', slug='qsfp28')
|
|
|
+ bay_type_a = ModuleBayType.objects.create(name='SFP28', slug='sfp28', manufacturer=manufacturer)
|
|
|
+ bay_type_b = ModuleBayType.objects.create(name='QSFP28', slug='qsfp28', manufacturer=manufacturer)
|
|
|
original = ModuleType.objects.create(manufacturer=manufacturer, model='Module Type 1')
|
|
|
original.module_bay_types.set([bay_type_a, bay_type_b])
|
|
|
|
|
|
@@ -564,12 +539,7 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
)
|
|
|
|
|
|
def test_module_bay_types_non_string_scalar_is_a_validation_error_not_a_crash(self):
|
|
|
- """
|
|
|
- A CSV cell is always a string, but this field is also bound from YAML-parsed data
|
|
|
- (e.g. via ModuleBayTemplateImportForm), where a scalar column can arrive as a
|
|
|
- non-string (an int or bool). .split() on that would raise AttributeError -- an
|
|
|
- unhandled 500 rather than a form error.
|
|
|
- """
|
|
|
+ """A non-string scalar (e.g. from YAML) must produce a form error, not a crash."""
|
|
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
|
|
|
|
|
form = ModuleTypeImportForm({
|
|
|
@@ -581,13 +551,7 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
self.assertIn('module_bay_types', form.errors)
|
|
|
|
|
|
def test_module_bay_types_permits_a_different_manufacturers_type(self):
|
|
|
- """
|
|
|
- The UI (ModuleTypeForm) and REST API place no manufacturer restriction on
|
|
|
- module_bay_types -- a third-party module may legitimately declare compatibility with
|
|
|
- another manufacturer's proprietary bay type. Import must permit the same, and a type
|
|
|
- created this way must survive an export/re-import round trip rather than becoming
|
|
|
- permanently unimportable.
|
|
|
- """
|
|
|
+ """The UI/API place no manufacturer restriction on module_bay_types; import must match."""
|
|
|
juniper = Manufacturer.objects.create(name='Juniper', slug='juniper')
|
|
|
cisco = Manufacturer.objects.create(name='Cisco', slug='cisco')
|
|
|
cisco_bay_type = ModuleBayType.objects.create(name='SFP28', slug='sfp28-cisco', manufacturer=cisco)
|
|
|
@@ -612,12 +576,7 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
self.assertEqual(list(reimport_form.save().module_bay_types.all()), [cisco_bay_type])
|
|
|
|
|
|
def test_module_bay_types_rejects_ambiguous_name_across_two_foreign_manufacturers(self):
|
|
|
- """
|
|
|
- A single other-manufacturer match is permitted (see above), but if the name matches
|
|
|
- two or more *different* foreign manufacturers, there's no principled way to choose
|
|
|
- one -- silently picking whichever sorts first would create a wrong FK link with no
|
|
|
- signal to the importer. This must be refused rather than resolved arbitrarily.
|
|
|
- """
|
|
|
+ """A name matching two different foreign manufacturers must be refused, not resolved arbitrarily."""
|
|
|
juniper = Manufacturer.objects.create(name='Juniper', slug='juniper')
|
|
|
cisco = Manufacturer.objects.create(name='Cisco', slug='cisco')
|
|
|
arista = Manufacturer.objects.create(name='Arista', slug='arista')
|
|
|
@@ -630,7 +589,10 @@ class ModuleTypeImportFormTestCase(TestCase):
|
|
|
'module_bay_types': ['SFP28'],
|
|
|
})
|
|
|
self.assertFalse(form.is_valid())
|
|
|
- self.assertIn('module_bay_types', form.errors)
|
|
|
+ # Must name both manufacturers, not just error -- a plain invalid_choice would also
|
|
|
+ # pass assertIn() and mask a regression of the scoping removed in 6f3c537.
|
|
|
+ errors = form.errors.get('module_bay_types', [])
|
|
|
+ self.assertTrue(any('Cisco' in e and 'Arista' in e for e in errors), errors)
|
|
|
|
|
|
|
|
|
class ModuleFormTestCase(TestCase):
|