diff --git a/netbox_awx_plugin/synchronization.py b/netbox_awx_plugin/synchronization.py index 17a72a00f589f49018d1d7849f6c1af148fcb1db..4cacfe0c7a00a968c46f5937da7d3204387dee4b 100644 --- a/netbox_awx_plugin/synchronization.py +++ b/netbox_awx_plugin/synchronization.py @@ -92,7 +92,7 @@ class DeviceSerializer(serializers.BaseSerializer): serializer = InterfaceSerializer(interface) variables["netbox_interfaces"].append(serializer.data) return { - "name": instance.primary_ip4.dns_name, + "name": getattr(instance.primary_ip4, 'dns_name', instance.name), "description": instance.description, "enabled": instance.status == DeviceStatusChoices.STATUS_ACTIVE, "variables": json.dumps(variables), @@ -116,16 +116,16 @@ class VMSerializer(serializers.BaseSerializer): def to_representation(self, instance): variables = { "netbox_virtualmachine_name": instance.name, - "netbox_virtualmachine_vcpus": float(instance.vcpus), - "netbox_virtualmachine_memory": instance.memory, - "netbox_virtualmachine_disk": instance.disk, + "netbox_virtualmachine_vcpus": float(instance.vcpus) if instance.vcpus is not None else 0.0, + "netbox_virtualmachine_memory": instance.memory or 0, + "netbox_virtualmachine_disk": instance.disk or 0, } variables["netbox_interfaces"] = [] for interface in instance.interfaces.all(): serializer = VMInterfaceSerializer(interface) variables["netbox_interfaces"].append(serializer.data) return { - "name": instance.primary_ip4.dns_name, + "name": getattr(instance.primary_ip4, 'dns_name', instance.name), "description": instance.description, "enabled": instance.status == VirtualMachineStatusChoices.STATUS_ACTIVE, "variables": json.dumps(variables), diff --git a/netbox_awx_plugin/tests/test_signals.py b/netbox_awx_plugin/tests/test_signals.py new file mode 100644 index 0000000000000000000000000000000000000000..b99a41edf6acdbd710b70cfed43ab9d9fd0a4040 --- /dev/null +++ b/netbox_awx_plugin/tests/test_signals.py @@ -0,0 +1,341 @@ +from django.test import TestCase +from unittest.mock import patch, call + +from dcim.models import Site, Device, DeviceRole, DeviceType, Manufacturer, Interface +from ipam.models import Prefix, IPAddress +from virtualization.models import VirtualMachine +from extras.models import Tag +from netbox_awx_plugin.models import AWX, AWXInventory +from netbox_awx_plugin.signals import enqueue_task +from django.db.models.signals import pre_save + +class SignalsTestCase(TestCase): + + def setUp(self): + # Create an AWX instance and inventory + self.awx = AWX.objects.create( + name='Test AWX', + url='https://awx.example.com', + token='token123' + ) + self.awx_inventory = AWXInventory.objects.create( + awx=self.awx, + inventory_id=1, + enabled=True, + ) + # Create a manufacturer for device types + self.manufacturer = Manufacturer.objects.create( + name='Test Manufacturer', + slug='test-manufacturer' + ) + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_site_post_save_signal(self, mock_enqueue_task): + # Create a Site instance + site = Site.objects.create( + name='Test Site', + slug='test-site', + status='active', + ) + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('sync_group', self.awx_inventory, Site, site) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_role_post_save_signal(self, mock_enqueue_task): + # Create a DeviceRole instance + device_role = DeviceRole.objects.create(name='Router', slug='router') + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('sync_group', self.awx_inventory, DeviceRole, device_role) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_type_post_save_signal(self, mock_enqueue_task): + # Create a DeviceType instance + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('sync_group', self.awx_inventory, DeviceType, device_type) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_prefix_post_save_signal(self, mock_enqueue_task): + # Create a Prefix instance + prefix = Prefix.objects.create(prefix='192.0.2.0/24') + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('sync_group', self.awx_inventory, Prefix, prefix) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_tag_post_save_signal(self, mock_enqueue_task): + # Create a Tag instance + tag = Tag.objects.create(name='Test Tag', slug='test-tag') + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('sync_group', self.awx_inventory, Tag, tag) + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_post_save_signal(self, mock_enqueue_task): + # Create related objects + device_role = DeviceRole.objects.create(name='Switch', slug='switch') + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + site = Site.objects.create(name='Test Site', slug='test-site', status='active') + + # Reset the mock after creating related objects + mock_enqueue_task.reset_mock() + + # Create a Device without primary IP + device = Device.objects.create( + name='Test Device', + device_role=device_role, + device_type=device_type, + site=site, + status='active', + ) + # Ensure enqueue_task was not called yet + mock_enqueue_task.assert_not_called() + + # Assign primary IP with DNS name + ip_address = IPAddress.objects.create( + address='192.0.2.1/24', + dns_name='test-device.example.com' + ) + device.primary_ip4 = ip_address + device.save() + + # Now enqueue_task should be called + mock_enqueue_task.assert_called_with('sync_host', self.awx_inventory, Device, device) + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_post_save_no_ip_dns(self, mock_enqueue_task): + # Create related objects + device_role = DeviceRole.objects.create(name='Switch', slug='switch') + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + site = Site.objects.create(name='Test Site', slug='test-site', status='active') + + # Reset the mock after creating related objects + mock_enqueue_task.reset_mock() + + # Create a Device without primary IP and DNS name + device = Device.objects.create( + name='Test Device', + device_role=device_role, + device_type=device_type, + site=site, + status='active', + ) + # Ensure enqueue_task was not called + mock_enqueue_task.assert_not_called() + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_tag_assignment(self, mock_enqueue_task): + # Create a Tag and assign it to a Device + tag = Tag.objects.create(name='Test Tag', slug='test-tag') + device_role = DeviceRole.objects.create(name='Switch', slug='switch') + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + site = Site.objects.create(name='Test Site', slug='test-site', status='active') + device = Device.objects.create( + name='Test Device', + device_role=device_role, + device_type=device_type, + site=site, + status='active', + ) + + # Reset the mock after creating related objects + mock_enqueue_task.reset_mock() + + device.tags.add(tag) + device.save() + + # enqueue_task should be called due to tag assignment + mock_enqueue_task.assert_called_with('sync_host', self.awx_inventory, Device, device) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_site_post_delete_signal(self, mock_enqueue_task): + # Create a Site instance + site = Site.objects.create( + name='Test Site', + slug='test-site', + status='active', + ) + site.delete() + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('delete_group', self.awx_inventory, Site, site) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_role_post_delete_signal(self, mock_enqueue_task): + # Create a DeviceRole instance + device_role = DeviceRole.objects.create(name='Router', slug='router') + device_role.delete() + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('delete_group', self.awx_inventory, DeviceRole, device_role) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_device_type_post_delete_signal(self, mock_enqueue_task): + # Create a DeviceType instance + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + device_type.delete() + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('delete_group', self.awx_inventory, DeviceType, device_type) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_prefix_post_delete_signal(self, mock_enqueue_task): + # Create a Prefix instance + prefix = Prefix.objects.create(prefix='192.0.2.0/24') + prefix.delete() + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('delete_group', self.awx_inventory, Prefix, prefix) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_tag_post_delete_signal(self, mock_enqueue_task): + # Create a Tag instance + tag = Tag.objects.create(name='Test Tag', slug='test-tag') + tag.delete() + # Check that enqueue_task was called + mock_enqueue_task.assert_called_with('delete_group', self.awx_inventory, Tag, tag) + + + def test_handle_group_pre_save(self): + # Create a Site instance + site = Site.objects.create( + name='Original Name', + slug='original-slug', + status='active', + ) + site.name = 'Updated Name' + # Manually send pre_save signal + pre_save.send(sender=Site, instance=site) + # Check that the original instance is stored + self.assertEqual(site._original.name, 'Original Name') + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_virtual_machine_post_save_signal(self, mock_enqueue_task): + # Create a VirtualMachine instance + vm = VirtualMachine.objects.create(name='Test VM', status='active') + # Ensure enqueue_task was not called yet + mock_enqueue_task.assert_not_called() + + # Assign primary IP with DNS name + ip_address = IPAddress.objects.create( + address='192.0.2.2/24', + dns_name='test-vm.example.com' + ) + vm.primary_ip4 = ip_address + vm.save() + + # Now enqueue_task should be called + mock_enqueue_task.assert_called_with('sync_host', self.awx_inventory, VirtualMachine, vm) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_interface_post_save_signal(self, mock_enqueue_task): + # Create related objects + device_role = DeviceRole.objects.create(name='Switch', slug='switch') + device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + site = Site.objects.create(name='Test Site', slug='test-site', status='active') + device = Device.objects.create( + name='Test Device', + device_role=device_role, + device_type=device_type, + site=site, + status='active', + ) + + # Assign primary IP with DNS name to the device + ip_address = IPAddress.objects.create( + address='192.0.2.1/24', + dns_name='test-device.example.com' + ) + device.primary_ip4 = ip_address + device.save() + + # Reset the mock after setting up device + mock_enqueue_task.reset_mock() + + # Create an Interface + interface = Interface.objects.create(device=device, name='eth0') + + # Check that enqueue_task was called due to interface save + mock_enqueue_task.assert_called_with('sync_host', self.awx_inventory, Device, device) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_vm_tag_assignment(self, mock_enqueue_task): + # Create a Tag and assign it to a VirtualMachine + tag = Tag.objects.create(name='Test Tag', slug='test-tag') + vm = VirtualMachine.objects.create(name='Test VM', status='active') + + # Reset the mock after creating related objects + mock_enqueue_task.reset_mock() + + vm.tags.add(tag) + vm.save() + + # enqueue_task should be called due to tag assignment + mock_enqueue_task.assert_called_with('sync_host', self.awx_inventory, VirtualMachine, vm) + + + @patch('netbox_awx_plugin.signals.enqueue_task') + def test_multiple_inventories(self, mock_enqueue_task): + # Create another AWX Inventory + another_awx_inventory = AWXInventory.objects.create( + awx=self.awx, + inventory_id=2, + enabled=True, + ) + # Create a Site instance + site = Site.objects.create( + name='Test Site', + slug='test-site', + status='active', + ) + # Check that enqueue_task was called for both inventories + calls = [ + call('sync_group', self.awx_inventory, Site, site), + call('sync_group', another_awx_inventory, Site, site) + ] + mock_enqueue_task.assert_has_calls(calls, any_order=True) + + + def test_no_enabled_inventories(self): + # Disable the AWX Inventory + self.awx_inventory.enabled = False + self.awx_inventory.save() + # Create a Site instance + with patch('netbox_awx_plugin.signals.enqueue_task') as mock_enqueue_task: + site = Site.objects.create( + name='Test Site', + slug='test-site', + status='active', + ) + # Check that enqueue_task was not called + mock_enqueue_task.assert_not_called() diff --git a/netbox_awx_plugin/tests/test_synchronization.py b/netbox_awx_plugin/tests/test_synchronization.py new file mode 100644 index 0000000000000000000000000000000000000000..8688323d400366f191331c6d4bd5de9449fb0d06 --- /dev/null +++ b/netbox_awx_plugin/tests/test_synchronization.py @@ -0,0 +1,300 @@ +# tests/test_synchronization.py + +from django.test import TestCase +from unittest.mock import patch, Mock, ANY +from netbox_awx_plugin.models import AWX, AWXInventory +from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site +from virtualization.models import VirtualMachine +from ipam.models import IPAddress +from extras.models import Tag +from netbox_awx_plugin.synchronization import ( + sync_host, + sync_group, + delete_host, + delete_group, + sync_all, + sync_host_group_association, + disassociate_removed_groups, +) +from netbox_awx_plugin.synchronization import serializers, group_prefixes +from django.contrib.contenttypes.models import ContentType + + +class SynchronizationTestCase(TestCase): + + def setUp(self): + # Create an AWX instance and inventory + self.awx = AWX.objects.create( + name='Test AWX', + url='https://awx.example.com', + token='token123' + ) + self.awx_inventory = AWXInventory.objects.create( + awx=self.awx, + inventory_id=1, + enabled=True, + ) + # Create related objects + self.manufacturer = Manufacturer.objects.create( + name='Test Manufacturer', + slug='test-manufacturer' + ) + self.device_role = DeviceRole.objects.create(name='Switch', slug='switch') + self.device_type = DeviceType.objects.create( + model='TestModel', + slug='testmodel', + manufacturer=self.manufacturer, + ) + self.site = Site.objects.create(name='Test Site', slug='test-site', status='active') + self.device = Device.objects.create( + name='Test Device', + device_role=self.device_role, + device_type=self.device_type, + site=self.site, + status='active', + ) + self.ip_address = IPAddress.objects.create( + address='192.0.2.1/24', + dns_name='test-device.example.com' + ) + self.device.primary_ip4 = self.ip_address + self.device.save() + + @patch('netbox_awx_plugin.models.AWXInventory.disassociate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.associate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.create_host') + @patch('netbox_awx_plugin.models.AWXInventory.update_host') + def test_sync_host_create(self, mock_update_host, mock_create_host, mock_get_host, mock_get_group, mock_associate_host_group, mock_disassociate_host_group): + # Simulate that the host does not exist on first call, but exists after creation + mock_get_host.side_effect = [ + None, # First call returns None + { + 'id': 1, + 'name': 'test-device.example.com', + 'summary_fields': {'groups': {'results': []}} + } # Second call returns a mock host + ] + # Simulate that the group exists + mock_get_group.return_value = {'id': 2, 'name': 'site_test_site'} + sync_host(self.awx_inventory, Device, self.device) + mock_create_host.assert_called() + mock_update_host.assert_not_called() + mock_associate_host_group.assert_called_with(1, 2) + + @patch('netbox_awx_plugin.models.AWXInventory.disassociate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.associate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.create_host') + @patch('netbox_awx_plugin.models.AWXInventory.update_host') + def test_sync_host_update(self, mock_update_host, mock_create_host, mock_get_host, mock_get_group, mock_associate_host_group, mock_disassociate_host_group): + # Simulate that the host exists + mock_get_host.return_value = { + 'id': 1, + 'name': 'test-device.example.com', + 'summary_fields': {'groups': {'results': []}} + } + # Simulate that the group exists + mock_get_group.return_value = {'id': 2, 'name': 'site_test_site'} + sync_host(self.awx_inventory, Device, self.device) + mock_update_host.assert_called() + mock_create_host.assert_not_called() + mock_associate_host_group.assert_called_with(1, 2) + + @patch('netbox_awx_plugin.models.AWXInventory.delete_host') + def test_delete_host(self, mock_delete_host): + delete_host(self.awx_inventory, Device, self.device) + mock_delete_host.assert_called_with('test-device.example.com') + + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + @patch('netbox_awx_plugin.models.AWXInventory.create_group') + @patch('netbox_awx_plugin.models.AWXInventory.update_group') + def test_sync_group_create(self, mock_update_group, mock_create_group, mock_get_group): + # Simulate that the group does not exist + mock_get_group.return_value = None + sync_group(self.awx_inventory, Site, self.site) + mock_create_group.assert_called() + mock_update_group.assert_not_called() + + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + @patch('netbox_awx_plugin.models.AWXInventory.create_group') + @patch('netbox_awx_plugin.models.AWXInventory.update_group') + def test_sync_group_update(self, mock_update_group, mock_create_group, mock_get_group): + # Simulate that the group exists but needs updating + mock_get_group.return_value = {'id': 1, 'name': 'site_test_site'} + sync_group(self.awx_inventory, Site, self.site) + mock_update_group.assert_called() + mock_create_group.assert_not_called() + + @patch('netbox_awx_plugin.models.AWXInventory.delete_group') + def test_delete_group(self, mock_delete_group): + delete_group(self.awx_inventory, Site, self.site) + mock_delete_group.assert_called_with('site_test_site') + + # Additional Tests + + @patch('netbox_awx_plugin.models.AWXInventory.update_host') + @patch('netbox_awx_plugin.models.AWXInventory.create_host') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.associate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + def test_sync_host_with_tags(self, mock_get_group, mock_associate_host_group, mock_get_host, mock_create_host, mock_update_host): + # Add a tag to the device + tag = Tag.objects.create(name='Test Tag', slug='test-tag') + self.device.tags.add(tag) + self.device.save() + # Simulate that the group for the tag exists + mock_get_group.return_value = {'id': 3, 'name': 'tag_test_tag'} + # Simulate that the host exists + mock_get_host.return_value = { + 'id': 1, + 'name': 'test-device.example.com', + 'summary_fields': {'groups': {'results': []}} + } + sync_host(self.awx_inventory, Device, self.device) + # Check that the host is associated with the tag group + mock_associate_host_group.assert_any_call(1, 3) + + @patch('netbox_awx_plugin.models.AWXInventory.disassociate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + def test_disassociate_removed_groups(self, mock_get_group, mock_get_host, mock_disassociate_host_group): + # Simulate that the host is associated with groups that are no longer valid + mock_get_host.return_value = { + 'id': 1, + 'name': 'test-device.example.com', + 'summary_fields': { + 'groups': { + 'results': [ + {'id': 2, 'name': 'site_old_site'}, + {'id': 3, 'name': 'devicerole_old_role'}, + {'id': 4, 'name': 'tag_old_tag'}, + ] + } + } + } + # Simulate that the group exists for the current site + mock_get_group.return_value = {'id': 5, 'name': 'site_test_site'} + # Assume the device only has site_test_site group now + sync_host(self.awx_inventory, Device, self.device) + # Check that the host is disassociated from 'site_old_site', 'devicerole_old_role', and 'tag_old_tag' + mock_disassociate_host_group.assert_any_call(1, 2) + mock_disassociate_host_group.assert_any_call(1, 3) + mock_disassociate_host_group.assert_any_call(1, 4) + + @patch('netbox_awx_plugin.models.AWXInventory.create_host') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.associate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + def test_sync_virtual_machine(self, mock_get_group, mock_associate_host_group, mock_get_host, mock_create_host): + # Create a role for the virtual machine + vm_content_type = ContentType.objects.get_for_model(VirtualMachine) + vm_role = DeviceRole.objects.create( + name='Web Server', + slug='web-server' + ) + # Create a virtual machine with a primary IP and a role + vm = VirtualMachine.objects.create( + name='Test VM', + status='active', + role=vm_role + ) + ip_address = IPAddress.objects.create( + address='192.0.2.2/24', + dns_name='test-vm.example.com' + ) + vm.primary_ip4 = ip_address + vm.save() + # Simulate that the host does not exist on first call, but exists after creation + mock_get_host.side_effect = [ + None, # First call returns None + { + 'id': 1, + 'name': 'test-vm.example.com', + 'summary_fields': {'groups': {'results': []}} + } # Second call returns a mock host + ] + # Simulate that the group exists for the VM role + mock_get_group.return_value = {'id': 2, 'name': 'devicerole_web_server'} + # Run the sync_host function + sync_host(self.awx_inventory, VirtualMachine, vm) + # Ensure that create_host was called + mock_create_host.assert_called_with(ANY) + # Ensure that the host is associated with the group + mock_associate_host_group.assert_called_with(1, 2) + + @patch('netbox_awx_plugin.models.AWXInventory.update_host') + @patch('netbox_awx_plugin.models.AWXInventory.get_host') + @patch('netbox_awx_plugin.models.AWXInventory.get_group') + @patch('netbox_awx_plugin.models.AWXInventory.associate_host_group') + @patch('netbox_awx_plugin.models.AWXInventory.disassociate_host_group') + def test_sync_host_missing_primary_ip(self, mock_disassociate_host_group, mock_associate_host_group, mock_get_group, mock_get_host, mock_update_host): + # Remove the primary IP from the device + self.device.primary_ip4 = None + self.device.save() + # Mock get_host to return a mock host + mock_get_host.return_value = { + 'id': 1, + 'name': self.device.name, + 'summary_fields': {'groups': {'results': []}} + } + # Mock get_group to prevent network calls + mock_get_group.return_value = {'id': 2, 'name': 'site_test_site'} + # Run the sync_host function + sync_host(self.awx_inventory, Device, self.device) + # Ensure that update_host was called + mock_update_host.assert_called_with(1, ANY) + # Ensure that associate_host_group was called + mock_associate_host_group.assert_called_with(1, 2) + + @patch('netbox_awx_plugin.synchronization.sync_host') + @patch('netbox_awx_plugin.synchronization.sync_group') + def test_sync_all(self, mock_sync_group, mock_sync_host): + job = Mock() + job.object = self.awx_inventory + sync_all(job) + # Ensure that sync_host and sync_group are called appropriately + self.assertTrue(mock_sync_group.called) + self.assertTrue(mock_sync_host.called) + + # Additional tests for helper functions + def test_sync_host_group_association(self): + # Create a mock inventory + inventory = Mock() + # Create a mock host + host = {'id': 1, 'name': 'test-device.example.com', 'summary_fields': {'groups': {'results': []}}} + # Create a mock group + group = {'id': 2, 'name': 'site_test_site'} + inventory.get_group.return_value = group + # Call the function + instance = self.device.site + sync_host_group_association(inventory, host, Site, instance, host['summary_fields']['groups']['results']) + # Check that associate_host_group was called + inventory.associate_host_group.assert_called_with(1, 2) + + def test_disassociate_removed_groups(self): + # Create a mock inventory + inventory = Mock() + # Create a mock host + host = { + 'id': 1, + 'name': 'test-device.example.com', + 'summary_fields': { + 'groups': { + 'results': [ + {'id': 2, 'name': 'site_old_site'}, + {'id': 3, 'name': 'devicerole_old_role'}, + {'id': 4, 'name': 'tag_old_tag'}, + ] + } + } + } + # Create an instance with current attributes + self.device.tags.clear() + disassociate_removed_groups(inventory, host, self.device, host['summary_fields']['groups']['results']) + # Check that disassociate_host_group was called for each group + inventory.disassociate_host_group.assert_any_call(1, 2) + inventory.disassociate_host_group.assert_any_call(1, 3) + inventory.disassociate_host_group.assert_any_call(1, 4)