"""Tests for the Hostgroup class in the hostgroups module.""" import unittest from unittest.mock import MagicMock, patch from netbox_zabbix_sync.modules.exceptions import HostgroupError from netbox_zabbix_sync.modules.hostgroups import Hostgroup class TestHostgroups(unittest.TestCase): """Test class for Hostgroup functionality.""" def setUp(self): """Set up test fixtures.""" # Create mock logger self.mock_logger = MagicMock() # *** Mock NetBox Device setup *** # Create mock device with all properties self.mock_device = MagicMock() self.mock_device.name = "test-device" # Set up site information site = MagicMock() site.name = "TestSite" # Set up region information region = MagicMock() region.name = "TestRegion" # Ensure region string representation returns the name region.__str__.return_value = "TestRegion" site.region = region # Set up site group information site_group = MagicMock() site_group.name = "TestSiteGroup" # Ensure site group string representation returns the name site_group.__str__.return_value = "TestSiteGroup" site.group = site_group self.mock_device.site = site # Set up role information (varies based on NetBox version) self.mock_device_role = MagicMock() self.mock_device_role.name = "TestRole" # Ensure string representation returns the name self.mock_device_role.__str__.return_value = "TestRole" self.mock_device.device_role = self.mock_device_role self.mock_device.role = self.mock_device_role # Set up tenant information tenant = MagicMock() tenant.name = "TestTenant" # Ensure tenant string representation returns the name tenant.__str__.return_value = "TestTenant" tenant_group = MagicMock() tenant_group.name = "TestTenantGroup" # Ensure tenant group string representation returns the name tenant_group.__str__.return_value = "TestTenantGroup" tenant.group = tenant_group self.mock_device.tenant = tenant # Set up platform information platform = MagicMock() platform.name = "TestPlatform" self.mock_device.platform = platform # Device-specific properties device_type = MagicMock() manufacturer = MagicMock() manufacturer.name = "TestManufacturer" device_type.manufacturer = manufacturer self.mock_device.device_type = device_type location = MagicMock() location.name = "TestLocation" # Ensure location string representation returns the name location.__str__.return_value = "TestLocation" self.mock_device.location = location rack = MagicMock() rack.name = "TestRack" self.mock_device.rack = rack # Custom fields — empty_cf is intentionally None to test the empty CF path self.mock_device.custom_fields = {"test_cf": "TestCF", "empty_cf": None} # *** Mock NetBox VM setup *** # Create mock VM with all properties self.mock_vm = MagicMock() self.mock_vm.name = "test-vm" # Reuse site from device self.mock_vm.site = site # Set up role for VM self.mock_vm.role = self.mock_device_role # Set up tenant for VM (same as device) self.mock_vm.tenant = tenant # Set up platform for VM (same as device) self.mock_vm.platform = platform # VM-specific properties cluster = MagicMock() cluster.name = "TestCluster" cluster_type = MagicMock() cluster_type.name = "TestClusterType" cluster.type = cluster_type self.mock_vm.cluster = cluster # Custom fields self.mock_vm.custom_fields = {"test_cf": "TestCF"} # Mock data for nesting tests self.mock_regions_data = [ {"name": "ParentRegion", "parent": None, "_depth": 0}, {"name": "TestRegion", "parent": "ParentRegion", "_depth": 1}, ] self.mock_groups_data = [ {"name": "ParentSiteGroup", "parent": None, "_depth": 0}, {"name": "TestSiteGroup", "parent": "ParentSiteGroup", "_depth": 1}, ] def test_device_hostgroup_creation(self): """Test basic device hostgroup creation.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Test the string representation self.assertEqual(str(hostgroup), "Hostgroup for dev test-device") # Check format options were set correctly self.assertEqual(hostgroup.format_options["site"], "TestSite") self.assertEqual(hostgroup.format_options["region"], "TestRegion") self.assertEqual(hostgroup.format_options["site_group"], "TestSiteGroup") self.assertEqual(hostgroup.format_options["role"], "TestRole") self.assertEqual(hostgroup.format_options["tenant"], "TestTenant") self.assertEqual(hostgroup.format_options["tenant_group"], "TestTenantGroup") self.assertEqual(hostgroup.format_options["platform"], "TestPlatform") self.assertEqual(hostgroup.format_options["manufacturer"], "TestManufacturer") self.assertEqual(hostgroup.format_options["location"], "TestLocation") self.assertEqual(hostgroup.format_options["rack"], "TestRack") def test_vm_hostgroup_creation(self): """Test basic VM hostgroup creation.""" hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger) # Test the string representation self.assertEqual(str(hostgroup), "Hostgroup for vm test-vm") # Check format options were set correctly self.assertEqual(hostgroup.format_options["site"], "TestSite") self.assertEqual(hostgroup.format_options["region"], "TestRegion") self.assertEqual(hostgroup.format_options["site_group"], "TestSiteGroup") self.assertEqual(hostgroup.format_options["role"], "TestRole") self.assertEqual(hostgroup.format_options["tenant"], "TestTenant") self.assertEqual(hostgroup.format_options["tenant_group"], "TestTenantGroup") self.assertEqual(hostgroup.format_options["platform"], "TestPlatform") self.assertEqual(hostgroup.format_options["cluster"], "TestCluster") self.assertEqual(hostgroup.format_options["cluster_type"], "TestClusterType") def test_invalid_object_type(self): """Test that an invalid object type raises an exception.""" with self.assertRaises(HostgroupError): Hostgroup("invalid", self.mock_device, "4.0", self.mock_logger) def test_device_hostgroup_formats(self): """Test different hostgroup formats for devices.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Custom format: site/region custom_result = hostgroup.generate("site/region") self.assertEqual(custom_result, "TestSite/TestRegion") # Custom format: site/tenant/platform/location complex_result = hostgroup.generate("site/tenant/platform/location") self.assertEqual( complex_result, "TestSite/TestTenant/TestPlatform/TestLocation" ) def test_vm_hostgroup_formats(self): """Test different hostgroup formats for VMs.""" hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger) # Default format: cluster/role default_result = hostgroup.generate("cluster/role") self.assertEqual(default_result, "TestCluster/TestRole") # Custom format: site/tenant custom_result = hostgroup.generate("site/tenant") self.assertEqual(custom_result, "TestSite/TestTenant") # Custom format: cluster/cluster_type/platform complex_result = hostgroup.generate("cluster/cluster_type/platform") self.assertEqual(complex_result, "TestCluster/TestClusterType/TestPlatform") def test_device_netbox_version_differences(self): """Test hostgroup generation with different NetBox versions. device_role (v2/v3) and role (v4+) are set to different values so the test can verify that the correct attribute is read for each version. """ # Build a device with deliberately different names on each role attribute versioned_device = MagicMock() versioned_device.name = "versioned-device" versioned_device.site = self.mock_device.site versioned_device.tenant = self.mock_device.tenant versioned_device.platform = self.mock_device.platform versioned_device.location = self.mock_device.location versioned_device.rack = self.mock_device.rack versioned_device.device_type = self.mock_device.device_type versioned_device.custom_fields = self.mock_device.custom_fields old_role = MagicMock() old_role.name = "OldRole" new_role = MagicMock() new_role.name = "NewRole" versioned_device.device_role = old_role # read by NetBox v2 / v3 code path versioned_device.role = new_role # read by NetBox v4+ code path # v2 must use device_role hostgroup_v2 = Hostgroup("dev", versioned_device, "2.11", self.mock_logger) self.assertEqual(hostgroup_v2.format_options["role"], "OldRole") # v3 must also use device_role hostgroup_v3 = Hostgroup("dev", versioned_device, "3.5", self.mock_logger) self.assertEqual(hostgroup_v3.format_options["role"], "OldRole") # v4+ must use role hostgroup_v4 = Hostgroup("dev", versioned_device, "4.0", self.mock_logger) self.assertEqual(hostgroup_v4.format_options["role"], "NewRole") def test_custom_field_lookup(self): """Test custom field lookup functionality.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Test custom field exists and is populated cf_result = hostgroup.custom_field_lookup("test_cf") self.assertTrue(cf_result["result"]) self.assertEqual(cf_result["cf"], "TestCF") # Test custom field doesn't exist cf_result = hostgroup.custom_field_lookup("nonexistent_cf") self.assertFalse(cf_result["result"]) self.assertIsNone(cf_result["cf"]) # Test custom field exists but has no value (None) cf_result = hostgroup.custom_field_lookup("empty_cf") self.assertTrue(cf_result["result"]) # key is present self.assertIsNone(cf_result["cf"]) # value is empty def test_hostgroup_with_custom_field(self): """Test hostgroup generation including a custom field.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Generate with custom field included result = hostgroup.generate("site/test_cf/role") self.assertEqual(result, "TestSite/TestCF/TestRole") def test_missing_hostgroup_format_item(self): """Test handling of missing hostgroup format items.""" # Create a device with minimal attributes minimal_device = MagicMock() minimal_device.name = "minimal-device" minimal_device.site = None minimal_device.tenant = None minimal_device.platform = None minimal_device.custom_fields = {} # Create role role = MagicMock() role.name = "MinimalRole" minimal_device.role = role # Create device_type with manufacturer device_type = MagicMock() manufacturer = MagicMock() manufacturer.name = "MinimalManufacturer" device_type.manufacturer = manufacturer minimal_device.device_type = device_type # Create hostgroup hostgroup = Hostgroup("dev", minimal_device, "4.0", self.mock_logger) # Generate with default format result = hostgroup.generate("site/manufacturer/role") # Site is missing, so only manufacturer and role should be included self.assertEqual(result, "MinimalManufacturer/MinimalRole") # Test with invalid format with self.assertRaises(HostgroupError): hostgroup.generate("site/nonexistent/role") def test_nested_region_hostgroups(self): """Test hostgroup generation with nested regions.""" # Mock the build_path function to return a predictable result with patch( "netbox_zabbix_sync.modules.hostgroups.build_path" ) as mock_build_path: # Configure the mock to return a list of regions in the path mock_build_path.return_value = ["ParentRegion", "TestRegion"] # Create hostgroup with nested regions enabled hostgroup = Hostgroup( "dev", self.mock_device, "4.0", self.mock_logger, nested_region_flag=True, nb_regions=self.mock_regions_data, ) # Generate hostgroup with region result = hostgroup.generate("site/region/role") # Should include the parent region self.assertEqual(result, "TestSite/ParentRegion/TestRegion/TestRole") def test_nested_sitegroup_hostgroups(self): """Test hostgroup generation with nested site groups.""" # Mock the build_path function to return a predictable result with patch( "netbox_zabbix_sync.modules.hostgroups.build_path" ) as mock_build_path: # Configure the mock to return a list of site groups in the path mock_build_path.return_value = ["ParentSiteGroup", "TestSiteGroup"] # Create hostgroup with nested site groups enabled hostgroup = Hostgroup( "dev", self.mock_device, "4.0", self.mock_logger, nested_sitegroup_flag=True, nb_groups=self.mock_groups_data, ) # Generate hostgroup with site_group result = hostgroup.generate("site/site_group/role") # Should include the parent site group self.assertEqual(result, "TestSite/ParentSiteGroup/TestSiteGroup/TestRole") def test_vm_list_based_hostgroup_format(self): """Test VM hostgroup generation with a list-based format.""" hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger) # Test with a list of format strings format_list = ["platform", "role", "cluster_type/cluster"] # Generate hostgroups for each format in the list hostgroups = [] for fmt in format_list: result = hostgroup.generate(fmt) if result: # Only add non-None results hostgroups.append(result) # Verify each expected hostgroup is generated self.assertEqual(len(hostgroups), 3) # Should have 3 hostgroups self.assertIn("TestPlatform", hostgroups) self.assertIn("TestRole", hostgroups) self.assertIn("TestClusterType/TestCluster", hostgroups) def test_nested_format_splitting(self): """Test that formats with slashes correctly split and resolve each component.""" hostgroup = Hostgroup("vm", self.mock_vm, "4.0", self.mock_logger) # Test a format with slashes that should be split complex_format = "cluster_type/cluster" result = hostgroup.generate(complex_format) # Verify the format is correctly split and each component resolved self.assertEqual(result, "TestClusterType/TestCluster") def test_multiple_hostgroup_formats_device(self): """Test device hostgroup generation with multiple formats.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Test with various formats that would be in a list formats = [ "site", "manufacturer/role", "platform/location", "tenant_group/tenant", ] # Generate and check each format results = {} for fmt in formats: results[fmt] = hostgroup.generate(fmt) # Verify results self.assertEqual(results["site"], "TestSite") self.assertEqual(results["manufacturer/role"], "TestManufacturer/TestRole") self.assertEqual(results["platform/location"], "TestPlatform/TestLocation") self.assertEqual(results["tenant_group/tenant"], "TestTenantGroup/TestTenant") def test_literal_string_in_format(self): """Test that quoted literal strings in a format are used verbatim.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # Single-quoted literal result = hostgroup.generate("'MyDevices'/role") self.assertEqual(result, "MyDevices/TestRole") # Double-quoted literal result = hostgroup.generate('"MyDevices"/role') self.assertEqual(result, "MyDevices/TestRole") def test_generate_returns_none_when_all_fields_empty(self): """Test that generate() returns None when every format field resolves to no value.""" empty_device = MagicMock() empty_device.name = "empty-device" empty_device.site = None empty_device.tenant = None empty_device.platform = None empty_device.role = None empty_device.location = None empty_device.rack = None empty_device.custom_fields = {} device_type = MagicMock() manufacturer = MagicMock() manufacturer.name = "SomeManufacturer" device_type.manufacturer = manufacturer empty_device.device_type = device_type hostgroup = Hostgroup("dev", empty_device, "4.0", self.mock_logger) # site, tenant and platform all have no value → hg_output stays empty → None result = hostgroup.generate("site/tenant/platform") self.assertIsNone(result) def test_vm_without_cluster(self): """Test that cluster/cluster_type are absent from format_options when VM has no cluster.""" clusterless_vm = MagicMock() clusterless_vm.name = "clusterless-vm" clusterless_vm.site = self.mock_vm.site clusterless_vm.tenant = self.mock_vm.tenant clusterless_vm.platform = self.mock_vm.platform clusterless_vm.role = self.mock_device_role clusterless_vm.cluster = None clusterless_vm.custom_fields = {} hostgroup = Hostgroup("vm", clusterless_vm, "4.0", self.mock_logger) # cluster and cluster_type must not appear in format_options self.assertNotIn("cluster", hostgroup.format_options) self.assertNotIn("cluster_type", hostgroup.format_options) # Requesting cluster in a format must raise HostgroupError with self.assertRaises(HostgroupError): hostgroup.generate("cluster/role") def test_empty_custom_field_skipped_in_format(self): """Test that an empty (None) custom field is silently omitted from the hostgroup name.""" hostgroup = Hostgroup("dev", self.mock_device, "4.0", self.mock_logger) # empty_cf has no value → it is skipped; only site and role appear result = hostgroup.generate("site/empty_cf/role") self.assertEqual(result, "TestSite/TestRole") if __name__ == "__main__": unittest.main()