def setUp(self): super().setUp() self.invalid_object_pks = [ uuid.uuid4(), uuid.uuid4(), ] self.invalid_relationship_associations = [ RelationshipAssociation( relationship=self.invalid_relationships[0], source=self.sites[1], destination_type=self.invalid_ct, destination_id=self.invalid_object_pks[1], ), RelationshipAssociation( relationship=self.invalid_relationships[1], source_type=self.invalid_ct, source_id=self.invalid_object_pks[0], destination=self.sites[1], ), RelationshipAssociation( relationship=self.invalid_relationships[2], source_type=self.invalid_ct, source_id=self.invalid_object_pks[0], destination_type=self.invalid_ct, destination_id=self.invalid_object_pks[1], ), ] for cra in self.invalid_relationship_associations: cra.validated_save()
def test_clean_wrong_type(self): # Create with the wrong source Type with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.m2m_1, source=self.sites[0], destination=self.vlans[0]) cra.clean() expected_errors = { "source_type": ["source_type has a different value than defined in Vlan to Rack"] } self.assertEqual(handler.exception.message_dict, expected_errors) # Create with the wrong destination Type with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.racks[0]) cra.clean() expected_errors = { "destination_type": [ "destination_type has a different value than defined in Vlan to Rack" ] } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_str(self): """Validate that the str() method works correctly.""" associations = [ RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[1]), RelationshipAssociation(relationship=self.o2os_1, source=self.racks[0], destination=self.racks[1]), ] for association in associations: association.validated_save() self.assertEqual(str(associations[0]), f"{self.racks[0]} -> {self.sites[1]} - {self.o2o_1}") self.assertEqual( str(associations[1]), f"{self.racks[0]} <-> {self.racks[1]} - {self.o2os_1}") self.assertEqual( str(self.invalid_relationship_associations[0]), f"{self.sites[1]} -> unknown - {self.invalid_relationships[0]}", ) self.assertEqual( str(self.invalid_relationship_associations[1]), f"unknown -> {self.sites[1]} - {self.invalid_relationships[1]}", ) self.assertEqual( str(self.invalid_relationship_associations[2]), f"unknown <-> unknown - {self.invalid_relationships[2]}", )
def test_clean_check_quantity_o2m(self): """Validate that one-to-many relationships can't have more than one relationship association per source.""" cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[0]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[1]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[1], destination=self.vlans[2]) cra.clean() cra.save() with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[2], destination=self.vlans[0]) cra.clean() expected_errors = { "destination": [ "Unable to create more than one generic site to vlan association to VLAN A (100) (destination)", ], } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_clean_wrong_type(self): # Create with the wrong source Type with self.assertRaises(ValidationError): cra = RelationshipAssociation(relationship=self.m2m_1, source=self.sites[0], destination=self.vlans[0]) cra.clean() # Create with the wrong destination Type with self.assertRaises(ValidationError): cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.racks[0]) cra.clean()
def test_delete_cascade(self): """Verify that a RelationshipAssociation is deleted if either of the associated records is deleted.""" initial_count = RelationshipAssociation.objects.count() associations = [ RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]), RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[1]), RelationshipAssociation(relationship=self.m2m_1, source=self.racks[1], destination=self.vlans[0]), # Create an association loop just to make sure it works correctly on deletion RelationshipAssociation(relationship=self.o2o_2, source=self.sites[2], destination=self.sites[3]), RelationshipAssociation(relationship=self.o2o_2, source=self.sites[3], destination=self.sites[2]), ] for association in associations: association.validated_save() # Create a self-referential association as well; validated_save() would correctly reject this one as invalid RelationshipAssociation.objects.create(relationship=self.o2o_2, source=self.sites[4], destination=self.sites[4]) self.assertEqual(6 + initial_count, RelationshipAssociation.objects.count()) # Test automatic deletion of RelationshipAssociations when their 'source' object is deleted self.racks[0].delete() # Both relations involving racks[0] should have been deleted # The relation between racks[1] and vlans[0] should remain, as should the site relations self.assertEqual(4 + initial_count, RelationshipAssociation.objects.count()) # Test automatic deletion of RelationshipAssociations when their 'destination' object is deleted self.vlans[0].delete() # Site relation remains self.assertEqual(3 + initial_count, RelationshipAssociation.objects.count()) # Test automatic deletion of RelationshipAssociations when there's a loop of source/destination references self.sites[3].delete() self.assertEqual(1 + initial_count, RelationshipAssociation.objects.count()) # Test automatic deletion of RelationshipAssociations when the same object is both source and destination self.sites[4].delete() self.assertEqual(initial_count, RelationshipAssociation.objects.count())
def test_exception_not_raised_when_updating_instance_with_relationship_type_o2o_or_o2m( self): """Validate 'Unable to create more than one relationship-association...' not raise when updating instance with type one-to-one, symmetric-one-to-one, one-to-many relationship.""" # Assert Exception not raise updating source of RelationshipAssociation with one-to-many relationship type cra_1 = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[1]) cra_1.validated_save() cra_1.source = self.sites[1] cra_1.validated_save() self.assertEqual(cra_1.source, self.sites[1]) # Validate Exception not raised when calling .validated_save() on a RelationshipAssociation instance without making any update cra_1.validated_save() # Assert Exception not raise updating source of RelationshipAssociation with one-to-one relationship type cra_2 = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[0]) cra_2.validated_save() cra_2.source = self.racks[1] cra_2.validated_save() self.assertEqual(cra_2.source, self.racks[1]) # Assert Exception not raise updating destination of RelationshipAssociation with one-to-one relationship type cra_3 = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[2], destination=self.sites[2]) cra_3.validated_save() cra_3.destination = self.sites[4] cra_3.validated_save() self.assertEqual(cra_3.destination, self.sites[4]) # Assert Exception not raise updating destination of RelationshipAssociation with symmetric-one-to-one relationship type cra_4 = RelationshipAssociation(relationship=self.o2os_1, source=self.racks[0], destination=self.racks[2]) cra_4.validated_save() cra_4.destination = self.racks[1] cra_4.validated_save() self.assertEqual(cra_4.destination, self.racks[1])
def test_create_relationship_associations_invalid_1(self): """ A new record CANNOT create ONE_TO_ONE relations where its "destination" is already associated. """ # Existing ONE_TO_ONE relation RelationshipAssociation( relationship=self.relationship_1, source_type=self.relationship_1.source_type, source_id=self.device_1.pk, destination_type=self.relationship_1.destination_type, destination_id=self.ipaddress_1.pk, ).validated_save() # Can't associate New Device with IP Address 1 (already associated to Device 1) form = DeviceForm(data=dict( **self.device_form_base_data, **{ f"cr_{self.relationship_1.slug}__destination": self.ipaddress_1.pk })) self.assertFalse(form.is_valid()) self.assertEqual( "10.1.1.1/24 is already involved in a BGP Router-ID relationship", form.errors[f"cr_{self.relationship_1.slug}__destination"][0], ) # Can't associate new IP address with Device 1 (already associated with IP Address 1) form = IPAddressForm(data=dict( **self.ipaddress_form_base_data, ** {f"cr_{self.relationship_1.slug}__source": self.device_1.pk})) self.assertFalse(form.is_valid()) self.assertEqual( "Device 1 is already involved in a BGP Router-ID relationship", form.errors[f"cr_{self.relationship_1.slug}__source"][0], )
def test_create_relationship_associations_invalid_3(self): """ A new record CANNOT create ONE_TO_ONE_SYMMETRIC relations where its peer is already associated. """ # Existing ONE_TO_ONE_SYMMETRIC relation RelationshipAssociation( relationship=self.relationship_3, source_type=self.relationship_3.source_type, source_id=self.device_1.pk, destination_type=self.relationship_3.destination_type, destination_id=self.device_2.pk, ).validated_save() # Peer is already a source for this relationship form = DeviceForm(data=dict( **self.device_form_base_data, ** {f"cr_{self.relationship_3.slug}__peer": self.device_1.pk})) self.assertFalse(form.is_valid()) self.assertEqual( "Device 1 is already involved in a HA Device Peer relationship", form.errors[f"cr_{self.relationship_3.slug}__peer"][0], ) # Peer is already a destination for this relationship form = DeviceForm(data=dict( **self.device_form_base_data, ** {f"cr_{self.relationship_3.slug}__peer": self.device_2.pk})) self.assertFalse(form.is_valid()) self.assertEqual( "Device 2 is already involved in a HA Device Peer relationship", form.errors[f"cr_{self.relationship_3.slug}__peer"][0], )
def test_update_relationship_associations_valid_2(self): """ An existing record with an existing ONE_TO_ONE association can change its source. """ # Existing ONE_TO_ONE relation RelationshipAssociation( relationship=self.relationship_1, source_type=self.relationship_1.source_type, source_id=self.device_1.pk, destination_type=self.relationship_1.destination_type, destination_id=self.ipaddress_1.pk, ).validated_save() form = IPAddressForm( instance=self.ipaddress_1, data={ "address": self.ipaddress_1.address, "status": self.status_active, f"cr_{self.relationship_1.slug}__source": self.device_2.pk, }, ) self.assertTrue(form.is_valid(), form.errors) self.assertTrue(form.save()) # Existing ONE_TO_ONE relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_1, source_id=self.device_1.pk) RelationshipAssociation.objects.get(relationship=self.relationship_1, source_id=self.device_2.pk, destination_id=self.ipaddress_1.pk)
def test_update_relationship_associations_valid_3(self): """ An existing record with an existing ONE_TO_MANY association can change its source. """ # Existing ONE_TO_MANY relation RelationshipAssociation( relationship=self.relationship_2, source_type=self.relationship_2.source_type, source_id=self.device_1.pk, destination_type=self.relationship_2.destination_type, destination_id=self.vlangroup_1.pk, ).validated_save() form = VLANGroupForm( instance=self.vlangroup_1, data={ "name": self.vlangroup_1.name, "slug": self.vlangroup_1.slug, "site": self.site, f"cr_{self.relationship_2.slug}__source": self.device_2.pk, }, ) self.assertTrue(form.is_valid(), form.errors) self.assertTrue(form.save()) # Existing ONE_TO_MANY relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_2, source_id=self.device_1.pk) RelationshipAssociation.objects.get(relationship=self.relationship_2, source_id=self.device_2.pk, destination_id=self.vlangroup_1.pk)
def test_clean_check_quantity_m2m(self): """Validate that many-to-many relationship can have many relationship associations.""" cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[1]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[1], destination=self.vlans[2]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[2], destination=self.vlans[0]) cra.clean()
def test_create_relationship_associations_invalid_2(self): """ A new record CANNOT create ONE_TO_MANY relations where any of its "destinations" are already associated. """ # Existing ONE_TO_MANY relation RelationshipAssociation( relationship=self.relationship_2, source_type=self.relationship_2.source_type, source_id=self.device_1.pk, destination_type=self.relationship_2.destination_type, destination_id=self.vlangroup_1.pk, ).validated_save() # Can't associate New Device with VLAN Group 1 (already associated to Device 1) form = DeviceForm(data=dict( **self.device_form_base_data, **{ f"cr_{self.relationship_2.slug}__destination": [self.vlangroup_1.pk, self.vlangroup_2.pk] }, )) self.assertFalse(form.is_valid()) self.assertEqual( "VLAN Group 1 is already involved in a Device VLAN Groups relationship", form.errors[f"cr_{self.relationship_2.slug}__destination"][0], )
def test_get_peer(self): cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]) cra.save() self.assertEqual(cra.get_peer(self.racks[0]), self.vlans[0]) self.assertEqual(cra.get_peer(self.vlans[0]), self.racks[0]) self.assertEqual(cra.get_peer(self.vlans[1]), None)
def test_clean_check_quantity_o2m(self): """Validate that one-to-many relationships can't have more than one relationship association per source.""" cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[0]) cra.validated_save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[1]) cra.validated_save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[1], destination=self.vlans[2]) cra.validated_save() with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[2], destination=self.vlans[0]) cra.clean() expected_errors = { "destination": [ "Unable to create more than one generic site to vlan association to VLAN A (100) (destination)", ], } self.assertEqual(handler.exception.message_dict, expected_errors) # Shouldn't be possible to create another copy of the same RelationshipAssociation with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[0]) cra.validated_save() expected_errors = { "__all__": [ "Relationship association with this Relationship, Source type, Source id, Destination type " "and Destination id already exists." ], "destination": [ "Unable to create more than one generic site to vlan association to VLAN A (100) (destination)", ], } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_clean_check_quantity_o2m(self): """Validate that one-to-many relationships can't have more than one relationship association per source. """ cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[0]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[0], destination=self.vlans[1]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.o2m_1, source=self.sites[1], destination=self.vlans[2]) cra.clean() cra.save() with self.assertRaises(ValidationError): cra = RelationshipAssociation(relationship=self.o2o_1, source=self.sites[2], destination=self.vlans[0]) cra.clean()
def test_get_peer(self): """Validate that the get_peer() method works correctly.""" cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]) cra.validated_save() self.assertEqual(cra.get_peer(self.racks[0]), self.vlans[0]) self.assertEqual(cra.get_peer(self.vlans[0]), self.racks[0]) self.assertEqual(cra.get_peer(self.vlans[1]), None)
def test_clean_check_quantity_o2o(self): """Validate that one-to-one relationships can't have more than one relationship association per side.""" cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[0]) cra.clean() cra.save() cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[1], destination=self.sites[1]) cra.clean() cra.save() with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[2]) cra.clean() expected_errors = { "source": [ "Unable to create more than one Primary Rack per Site association to Rack A (source)" ] } self.assertEqual(handler.exception.message_dict, expected_errors) with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[2], destination=self.sites[0]) cra.clean() expected_errors = { "destination": [ "Unable to create more than one Primary Rack per Site association to Site A (destination)" ] } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_create_invalid_relationship_association(self): """Test creation of invalid relationship association restricted by destination/source filter.""" relationship = Relationship.objects.create( name="Site to Rack Rel 1", slug="site-to-rack-rel-1", source_type=self.site_ct, source_filter={"name": [self.sites[0].name]}, destination_type=self.rack_ct, destination_label="Primary Rack", type=RelationshipTypeChoices.TYPE_ONE_TO_ONE, destination_filter={"name": [self.racks[0].name]}, ) associations = ( ( "source", RelationshipAssociation(relationship=relationship, source=self.sites[1], destination=self.racks[0]), ), ( "destination", RelationshipAssociation(relationship=relationship, source=self.sites[0], destination=self.racks[1]), ), ) for side_name, association in associations: side = getattr(association, side_name) with self.assertRaises(ValidationError) as handler: association.validated_save() expected_errors = { side_name: [ f"{side} violates {relationship} {side_name}_filter restriction" ] } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_clean_check_quantity_m2m(self): """Validate that many-to-many relationship can have many relationship associations.""" cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]) cra.validated_save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[1]) cra.validated_save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[1], destination=self.vlans[2]) cra.validated_save() cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[2], destination=self.vlans[0]) cra.validated_save() # Shouldn't be possible to create another copy of the same RelationshipAssociation with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]) cra.validated_save() expected_errors = { "__all__": [ "Relationship association with this Relationship, Source type, Source id, Destination type " "and Destination id already exists." ], } self.assertEqual(handler.exception.message_dict, expected_errors) cra = RelationshipAssociation(relationship=self.m2ms_1, source=self.sites[0], destination=self.sites[1]) cra.validated_save() # Shouldn't be possible to create a mirrored copy of the same symmetric RelationshipAssociation with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.m2ms_1, source=self.sites[1], destination=self.sites[0]) cra.validated_save() expected_errors = { "__all__": [ "A Related Sites association already exists between Site B and Site A" ] } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_relationship_association_validator_raises_exception(self): status_active = Status.objects.create(name="status1", slug="status1") prefix = Prefix.objects.create( prefix=netaddr.IPNetwork("192.168.10.0/24")) ipaddress = IPAddress.objects.create(address="192.168.22.1/24", status=status_active) relationship = Relationship.objects.create( name="Test Relationship", slug="test-relationship", source_type=ContentType.objects.get_for_model(Prefix), destination_type=ContentType.objects.get_for_model(IPAddress), type=RelationshipTypeChoices.TYPE_ONE_TO_MANY, ) relationship_assoc = RelationshipAssociation(relationship=relationship, source=prefix, destination=ipaddress) with self.assertRaises(ValidationError): relationship_assoc.clean()
def test_update_relationship_associations_valid_4(self): """ An existing record with an existing ONE_TO_ONE_SYMMETRIC association can change its peer. This differs from test_update_relationship_associations_valid_1 in that the existing association has this record as the destination rather than the source, which *should* work either way. """ # Existing ONE_TO_ONE_SYMMETRIC relation RelationshipAssociation( relationship=self.relationship_3, source_type=self.relationship_3.source_type, source_id=self.device_3.pk, destination_type=self.relationship_3.destination_type, destination_id=self.device_1.pk, ).validated_save() form = DeviceForm( instance=self.device_1, data={ "site": self.site, "device_role": self.device_role, "device_type": self.device_type, "status": self.status_active, f"cr_{self.relationship_3.slug}__peer": self.device_2.pk, }, ) self.assertTrue(form.is_valid(), form.errors) self.assertTrue(form.save()) # Existing ONE_TO_ONE_SYMMETRIC relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_3, source_id=self.device_3.pk) RelationshipAssociation.objects.get( Q(source_id=self.device_1.pk, destination_id=self.device_2.pk) | Q(source_id=self.device_2.pk, destination_id=self.device_1.pk), relationship=self.relationship_3, )
def test_get_relationships_data(self): # In addition to the invalid associations for sites[1] defined in self.setUp(), add some valid ones associations = [ RelationshipAssociation(relationship=self.o2m_1, source=self.sites[1], destination=self.vlans[0]), RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[1]), RelationshipAssociation(relationship=self.o2o_2, source=self.sites[0], destination=self.sites[1]), ] for association in associations: association.validated_save() with self.assertLogs(logger=logging.getLogger( "nautobot.extras.models.relationships"), level="ERROR"): data = self.sites[1].get_relationships_data() self.maxDiff = None # assertEqual doesn't work well on the entire data at once because it includes things like queryset objects self.assertEqual(sorted(data.keys()), ["destination", "peer", "source"]) self.assertEqual( set(data["destination"].keys()), {self.o2o_1, self.o2o_2, self.invalid_relationships[1]}) self.assertEqual( data["destination"][self.o2o_1], { "has_many": False, "label": "Primary Rack", "peer_type": self.rack_ct, "url": reverse("dcim:rack", kwargs={"pk": self.racks[0].pk}), "value": self.racks[0], }, ) self.assertEqual( data["destination"][self.o2o_2], { "has_many": False, "label": "Alphabetically Subsequent", "peer_type": self.site_ct, "url": reverse("dcim:site", kwargs={"slug": self.sites[0].slug}), "value": self.sites[0], }, ) self.assertEqual( data["destination"][self.invalid_relationships[1]], { "has_many": False, "label": "Invalid Relationship 2", "peer_type": self.invalid_ct, "url": None, "value": None, }, ) self.assertEqual(set(data["peer"].keys()), {self.m2ms_1}) # Peer queryset is complex, but evaluates to an empty list in this case self.assertEqual(list(data["peer"][self.m2ms_1]["queryset"]), []) del data["peer"][self.m2ms_1]["queryset"] self.assertEqual( data["peer"][self.m2ms_1], { "has_many": True, "label": "sites", "peer_type": self.site_ct, "value": None, }, ) self.assertEqual( set(data["source"].keys()), {self.o2m_1, self.o2o_2, self.invalid_relationships[0]}) self.assertEqual(list(data["source"][self.o2m_1]["queryset"]), [associations[0]]) del data["source"][self.o2m_1]["queryset"] self.assertEqual( data["source"][self.o2m_1], { "has_many": True, "label": "VLANs", "peer_type": self.vlan_ct, "value": None, }, ) self.assertEqual( data["source"][self.o2o_2], { "has_many": False, "label": "Alphabetically Prior", "peer_type": self.site_ct, "url": None, "value": None, }, ) self.assertEqual( data["source"][self.invalid_relationships[0]], { "has_many": False, "label": "Invalid Relationship 1", "peer_type": self.invalid_ct, "url": None, # value is None because the related object can't actually be found "value": None, }, )
def setUpTestData(cls): cls.device_type = ContentType.objects.get_for_model(Device) cls.vlan_type = ContentType.objects.get_for_model(VLAN) cls.relationships = ( Relationship( name="Device VLANs", slug="device-vlans", type="many-to-many", source_type=cls.device_type, destination_type=cls.vlan_type, ), Relationship( name="Primary VLAN", slug="primary-vlan", type="one-to-many", source_type=cls.vlan_type, destination_type=cls.device_type, ), ) for relationship in cls.relationships: relationship.validated_save() manufacturer = Manufacturer.objects.create(name="Manufacturer 1", slug="manufacturer-1") devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1") devicerole = DeviceRole.objects.create(name="Device Role 1", slug="device-role-1") site = Site.objects.create(name="Site 1", slug="site-1") cls.devices = ( Device.objects.create(name="Device 1", device_type=devicetype, device_role=devicerole, site=site), Device.objects.create(name="Device 2", device_type=devicetype, device_role=devicerole, site=site), ) cls.vlans = ( VLAN.objects.create(vid=1, name="VLAN 1"), VLAN.objects.create(vid=2, name="VLAN 2"), ) RelationshipAssociation( relationship=cls.relationships[0], source_type=cls.device_type, source_id=cls.devices[0].pk, destination_type=cls.vlan_type, destination_id=cls.vlans[0].pk, ).validated_save() RelationshipAssociation( relationship=cls.relationships[0], source_type=cls.device_type, source_id=cls.devices[1].pk, destination_type=cls.vlan_type, destination_id=cls.vlans[1].pk, ).validated_save() RelationshipAssociation( relationship=cls.relationships[1], source_type=cls.vlan_type, source_id=cls.vlans[0].pk, destination_type=cls.device_type, destination_id=cls.devices[0].pk, ).validated_save() RelationshipAssociation( relationship=cls.relationships[1], source_type=cls.vlan_type, source_id=cls.vlans[1].pk, destination_type=cls.device_type, destination_id=cls.devices[1].pk, ).validated_save()
def test_clean_check_quantity_o2o(self): """Validate that one-to-one relationships can't have more than one relationship association per side.""" cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[0]) cra.validated_save() cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[1], destination=self.sites[1]) cra.validated_save() cra = RelationshipAssociation(relationship=self.o2os_1, source=self.racks[0], destination=self.racks[1]) cra.validated_save() with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[2]) cra.clean() expected_errors = { "source": [ "Unable to create more than one Primary Rack per Site association to Rack A (source)" ] } self.assertEqual(handler.exception.message_dict, expected_errors) with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2o_1, source=self.racks[2], destination=self.sites[0]) cra.clean() expected_errors = { "destination": [ "Unable to create more than one Primary Rack per Site association to Site A (destination)" ] } self.assertEqual(handler.exception.message_dict, expected_errors) with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2os_1, source=self.racks[0], destination=self.racks[2]) cra.clean() expected_errors = { "source": [ "Unable to create more than one Redundant Rack association to Rack A (source)" ] } self.assertEqual(handler.exception.message_dict, expected_errors) # Slightly tricky case - a symmetric one-to-one relationship where the proposed *source* is already in use # as a *destination* in a different RelationshipAssociation with self.assertRaises(ValidationError) as handler: cra = RelationshipAssociation(relationship=self.o2os_1, source=self.racks[1], destination=self.racks[2]) cra.clean() expected_errors = { "source": [ "Unable to create more than one Redundant Rack association involving Rack B (peer)" ] } self.assertEqual(handler.exception.message_dict, expected_errors)
def test_update_relationship_associations_valid_1(self): """ An existing record with an existing ONE_TO_ONE or ONE_TO_MANY association can change its destination(s). """ # Existing ONE_TO_ONE relation RelationshipAssociation( relationship=self.relationship_1, source_type=self.relationship_1.source_type, source_id=self.device_1.pk, destination_type=self.relationship_1.destination_type, destination_id=self.ipaddress_1.pk, ).validated_save() # Existing ONE_TO_MANY relation RelationshipAssociation( relationship=self.relationship_2, source_type=self.relationship_2.source_type, source_id=self.device_1.pk, destination_type=self.relationship_2.destination_type, destination_id=self.vlangroup_1.pk, ).validated_save() # Existing ONE_TO_ONE_SYMMETRIC relation RelationshipAssociation( relationship=self.relationship_3, source_type=self.relationship_3.source_type, source_id=self.device_1.pk, destination_type=self.relationship_3.destination_type, destination_id=self.device_3.pk, ).validated_save() form = DeviceForm( instance=self.device_1, data={ "site": self.site, "device_role": self.device_role, "device_type": self.device_type, "status": self.status_active, f"cr_{self.relationship_1.slug}__destination": self.ipaddress_2.pk, f"cr_{self.relationship_2.slug}__destination": [self.vlangroup_2.pk], f"cr_{self.relationship_3.slug}__peer": self.device_2.pk, }, ) self.assertTrue(form.is_valid(), form.errors) self.assertTrue(form.save()) # Existing ONE_TO_ONE relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_1, destination_id=self.ipaddress_1.pk) RelationshipAssociation.objects.get(relationship=self.relationship_1, source_id=self.device_1.pk, destination_id=self.ipaddress_2.pk) # Existing ONE_TO_MANY relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_2, destination_id=self.vlangroup_1.pk) RelationshipAssociation.objects.get(relationship=self.relationship_2, source_id=self.device_1.pk, destination_id=self.vlangroup_2.pk) # Existing ONE_TO_ONE_SYMMETRIC relation should have been deleted and replaced with self.assertRaises(RelationshipAssociation.DoesNotExist): RelationshipAssociation.objects.get( relationship=self.relationship_3, destination_id=self.device_3.pk) RelationshipAssociation.objects.get( Q(source_id=self.device_1.pk, destination_id=self.device_2.pk) | Q(source_id=self.device_2.pk, destination_id=self.device_1.pk), relationship=self.relationship_3, )
def test_generic_relation(self): """Verify that the GenericRelations on the involved models work correctly.""" associations = ( RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[0]), RelationshipAssociation(relationship=self.m2m_1, source=self.racks[0], destination=self.vlans[1]), RelationshipAssociation(relationship=self.o2o_1, source=self.racks[0], destination=self.sites[0]), ) for association in associations: association.validated_save() # Check that the GenericRelation lookup works correctly self.assertEqual(3, self.racks[0].source_for_associations.count()) self.assertEqual(0, self.racks[0].destination_for_associations.count()) self.assertEqual(0, self.vlans[0].source_for_associations.count()) self.assertEqual(1, self.vlans[0].destination_for_associations.count()) # Check that the related_query_names work correctly for each individual RelationshipAssociation self.assertEqual([self.racks[0]], list(associations[0].source_dcim_rack.all())) self.assertEqual([self.vlans[0]], list(associations[0].destination_ipam_vlan.all())) self.assertEqual([], list(associations[0].destination_dcim_site.all())) self.assertEqual([self.racks[0]], list(associations[1].source_dcim_rack.all())) self.assertEqual([self.vlans[1]], list(associations[1].destination_ipam_vlan.all())) self.assertEqual([], list(associations[1].destination_dcim_site.all())) self.assertEqual([self.racks[0]], list(associations[2].source_dcim_rack.all())) self.assertEqual([], list(associations[2].destination_ipam_vlan.all())) self.assertEqual([self.sites[0]], list(associations[2].destination_dcim_site.all())) # Check that the related query names can be used for filtering as well self.assertEqual( 3, RelationshipAssociation.objects.filter( source_dcim_rack=self.racks[0]).count()) self.assertEqual( 2, RelationshipAssociation.objects.filter( destination_ipam_vlan__isnull=False).count()) self.assertEqual( 1, RelationshipAssociation.objects.filter( destination_ipam_vlan=self.vlans[0]).count()) self.assertEqual( 1, RelationshipAssociation.objects.filter( destination_ipam_vlan=self.vlans[1]).count()) self.assertEqual( 1, RelationshipAssociation.objects.filter( destination_dcim_site=self.sites[0]).count())