11from django .test import TestCase
2+ from unittest .mock import MagicMock
3+
24from ..api .serializers import (
35 PrometheusDeviceSerializer ,
46 PrometheusIPAddressSerializer ,
57 PrometheusServiceSerializer ,
68 PrometheusVirtualMachineSerializer ,
79)
10+ from ..api .utils import LabelDict , extract_cluster
811from . import utils
912
1013from ..api .utils import NETBOX_RELEASE_CURRENT , NETBOX_RELEASE_41
1114
12- class PrometheusVirtualMachineSerializerTests (TestCase ):
15+ class DictSubsetMixin :
16+ """Mixin to provide assertDictContainsSubset which was removed in Python 3.12."""
17+
18+ def assertDictContainsSubset (self , subset , dictionary , msg = None ):
19+ """Check that all key/value pairs in subset are in dictionary."""
20+ for key , value in subset .items ():
21+ self .assertIn (key , dictionary , msg = msg )
22+ self .assertEqual (dictionary [key ], value , msg = msg )
23+
24+
25+ class PrometheusVirtualMachineSerializerTests (DictSubsetMixin , TestCase ):
1326 def test_vm_minimal_to_target (self ):
1427
1528 instance = utils .build_minimal_vm ("vm-01.example.com" )
@@ -102,23 +115,12 @@ def test_vm_full_to_target(self):
102115 if NETBOX_RELEASE_CURRENT > NETBOX_RELEASE_41 :
103116 self .assertTrue (
104117 utils .dictContainsSubset (
105- {"__meta_netbox_scope" : "Campus A" }, data ["labels" ]
106- )
107- )
108- self .assertTrue (
109- utils .dictContainsSubset (
110- {"__meta_netbox_scope_slug" : "campus-a" }, data ["labels" ]
111- )
112- )
113- else :
114- self .assertTrue (
115- utils .dictContainsSubset (
116- {"__meta_netbox_site" : "Campus A" }, data ["labels" ]
118+ {"__meta_netbox_cluster_scope" : "Campus A" }, data ["labels" ]
117119 )
118120 )
119121 self .assertTrue (
120122 utils .dictContainsSubset (
121- {"__meta_netbox_site_slug " : "campus-a" }, data ["labels" ]
123+ {"__meta_netbox_cluster_scope_slug " : "campus-a" }, data ["labels" ]
122124 )
123125 )
124126 self .assertTrue (
@@ -200,7 +202,7 @@ def test_vm_full_to_target(self):
200202 )
201203
202204
203- class PrometheusDeviceSerializerTests (TestCase ):
205+ class PrometheusDeviceSerializerTests (DictSubsetMixin , TestCase ):
204206 def test_device_minimal_to_target (self ):
205207 instance = utils .build_minimal_device ("firewall-01" )
206208 data = PrometheusDeviceSerializer (many = True , instance = [instance ]).data [0 ]
@@ -364,7 +366,7 @@ def test_device_full_to_target(self):
364366 )
365367
366368
367- class PrometheusIPAddressSerializerTests (TestCase ):
369+ class PrometheusIPAddressSerializerTests (DictSubsetMixin , TestCase ):
368370 def test_ip_minimal_to_target (self ):
369371 instance = utils .build_minimal_ip ("10.10.10.10/24" )
370372 data = PrometheusIPAddressSerializer (many = True , instance = [instance ]).data [0 ]
@@ -440,7 +442,7 @@ def test_ip_full_to_target(self):
440442 )
441443
442444
443- class PrometheusServiceSerializerTests (TestCase ):
445+ class PrometheusServiceSerializerTests (DictSubsetMixin , TestCase ):
444446 def test_device_service_full_to_target (self ):
445447 device = utils .build_device_full ("firewall-full-01" )
446448 instance = device .services .first ()
@@ -548,23 +550,12 @@ def test_vm_service_full_to_target(self):
548550 if NETBOX_RELEASE_CURRENT > NETBOX_RELEASE_41 :
549551 self .assertTrue (
550552 utils .dictContainsSubset (
551- {"__meta_netbox_scope " : "Campus A" }, data ["labels" ]
553+ {"__meta_netbox_cluster_scope " : "Campus A" }, data ["labels" ]
552554 )
553555 )
554556 self .assertTrue (
555557 utils .dictContainsSubset (
556- {"__meta_netbox_scope_slug" : "campus-a" }, data ["labels" ]
557- )
558- )
559- else :
560- self .assertTrue (
561- utils .dictContainsSubset (
562- {"__meta_netbox_site" : "Campus A" }, data ["labels" ]
563- )
564- )
565- self .assertTrue (
566- utils .dictContainsSubset (
567- {"__meta_netbox_site_slug" : "campus-a" }, data ["labels" ]
558+ {"__meta_netbox_cluster_scope_slug" : "campus-a" }, data ["labels" ]
568559 )
569560 )
570561 self .assertTrue (
@@ -582,3 +573,88 @@ def test_vm_service_full_to_target(self):
582573 {"__meta_netbox_primary_ip6" : "2001:db8:1701::2" }, data ["labels" ]
583574 )
584575 )
576+
577+
578+ class ExtractClusterTests (TestCase ):
579+ """Tests for extract_cluster function handling both NetBox 4.2+ scope and older site."""
580+
581+ def test_extract_cluster_basic_info (self ):
582+ """Test that basic cluster info (name, group, type) is extracted."""
583+ device = utils .build_device_with_cluster ("device-cluster-test" , cluster_name = "TestCluster" )
584+ labels = LabelDict ()
585+ extract_cluster (device , labels )
586+
587+ self .assertEqual (labels .get ("cluster" ), "TestCluster" )
588+ self .assertEqual (labels .get ("cluster_group" ), "VMware" )
589+ self .assertEqual (labels .get ("cluster_type" ), "On Prem" )
590+
591+ def test_extract_cluster_scope_from_scope (self ):
592+ """Test that scope labels are extracted from cluster scope (NetBox 4.2+)."""
593+ device = utils .build_device_with_cluster ("device-scope-test" , cluster_name = "ScopeCluster" )
594+ labels = LabelDict ()
595+ extract_cluster (device , labels )
596+
597+ # In NetBox 4.2+, cluster scope is labeled as 'cluster_scope'
598+ self .assertEqual (labels .get ("cluster_scope" ), "Cluster Site" )
599+ self .assertEqual (labels .get ("cluster_scope_slug" ), "cluster-site" )
600+ # Device's own site takes precedence for 'site' label
601+ self .assertEqual (labels .get ("site" ), "Site" )
602+ self .assertEqual (labels .get ("site_slug" ), "site" )
603+
604+ def test_extract_cluster_no_cluster (self ):
605+ """Test that no cluster labels are added when device has no cluster."""
606+ device = utils .build_minimal_device ("device-no-cluster-test" )
607+ labels = LabelDict ()
608+ extract_cluster (device , labels )
609+
610+ self .assertIsNone (labels .get ("cluster" ))
611+ self .assertIsNone (labels .get ("cluster_group" ))
612+ self .assertIsNone (labels .get ("cluster_type" ))
613+
614+ def test_extract_cluster_device_site_overrides_cluster_site (self ):
615+ """Test that device's own site takes precedence over cluster site."""
616+ device = utils .build_device_with_cluster ("device-site-override" , cluster_name = "OverrideCluster" )
617+ # Device's own site should override cluster site
618+ labels = LabelDict ()
619+ extract_cluster (device , labels )
620+
621+ # Device has its own site set in build_minimal_device ("Site", "site")
622+ # which should take precedence over the cluster's site
623+ self .assertEqual (labels .get ("site" ), "Site" )
624+ self .assertEqual (labels .get ("site_slug" ), "site" )
625+
626+ def test_extract_cluster_with_scope_mock (self ):
627+ """Test extract_cluster with mocked scope object (NetBox 4.2+ style)."""
628+ # Create a mock object to simulate NetBox 4.2+ cluster with scope
629+ mock_site = MagicMock ()
630+ mock_site .name = "Scoped Site"
631+ mock_site .slug = "scoped-site"
632+ mock_site .__class__ .__name__ = "Site"
633+
634+ mock_cluster = MagicMock ()
635+ mock_cluster .name = "MockCluster"
636+ mock_cluster .group = MagicMock (name = "MockGroup" )
637+ mock_cluster .group .name = "Mock Group"
638+ mock_cluster .type = MagicMock (name = "MockType" )
639+ mock_cluster .type .name = "Mock Type"
640+ mock_cluster .scope = mock_site
641+ # Ensure hasattr returns True for scope
642+ del mock_cluster .site
643+
644+ mock_obj = MagicMock ()
645+ mock_obj .cluster = mock_cluster
646+ # Remove site attribute from obj
647+ del mock_obj .site
648+
649+ labels = LabelDict ()
650+ extract_cluster (mock_obj , labels )
651+
652+ self .assertEqual (labels .get ("cluster" ), "MockCluster" )
653+ self .assertEqual (labels .get ("cluster_group" ), "Mock Group" )
654+ self .assertEqual (labels .get ("cluster_type" ), "Mock Type" )
655+ # In NetBox 4.2+, cluster scope is labeled as 'cluster_scope'
656+ self .assertEqual (labels .get ("cluster_scope" ), "Scoped Site" )
657+ self .assertEqual (labels .get ("cluster_scope_slug" ), "scoped-site" )
658+ # Since scope is a Site, site labels should also be set
659+ self .assertEqual (labels .get ("site" ), "Scoped Site" )
660+ self .assertEqual (labels .get ("site_slug" ), "scoped-site" )
0 commit comments