@@ -949,3 +949,222 @@ func TestEntityDetails(t *testing.T) {
949949 })
950950 })
951951}
952+
953+ // TestFetchEntityDescendentPaths_DirectPathsToRoot verifies that each terminal node with a direct
954+ // azure.Contains edge to the root tenant is included in the returned path set.
955+ func TestFetchEntityDescendentPaths_DirectPathsToRoot (t * testing.T ) {
956+ t .Parallel ()
957+
958+ suite := setupIntegrationTestSuite (t )
959+ defer teardownIntegrationTestSuite (t , & suite )
960+
961+ var (
962+ tenantID = integration .RandomObjectID (t )
963+ tenantNode = NewAzureTenant (t , & suite , tenantID )
964+ app1 = NewAzureApplication (t , & suite , "App1" , integration .RandomObjectID (t ), tenantID )
965+ app2 = NewAzureApplication (t , & suite , "App2" , integration .RandomObjectID (t ), tenantID )
966+ )
967+
968+ NewRelationship (t , & suite , tenantNode , app1 , graphAzure .Contains )
969+ NewRelationship (t , & suite , tenantNode , app2 , graphAzure .Contains )
970+
971+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
972+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
973+ require .NoError (t , err )
974+
975+ nodeIDs := paths .AllNodes ().IDs ()
976+ require .Len (t , nodeIDs , 3 )
977+ require .Contains (t , nodeIDs , tenantNode .ID )
978+ require .Contains (t , nodeIDs , app1 .ID )
979+ require .Contains (t , nodeIDs , app2 .ID )
980+
981+ return nil
982+ })
983+ require .NoError (t , err )
984+ }
985+
986+ // TestFetchEntityDescendentPaths_MultiHopPathToRoot verifies that a terminal node connected to the
987+ // root through one or more intermediate azure.Contains hops is fully represented in the path set,
988+ // with every intermediary node included.
989+ func TestFetchEntityDescendentPaths_MultiHopPathToRoot (t * testing.T ) {
990+ t .Parallel ()
991+
992+ suite := setupIntegrationTestSuite (t )
993+ defer teardownIntegrationTestSuite (t , & suite )
994+
995+ var (
996+ tenantID = integration .RandomObjectID (t )
997+ tenantNode = NewAzureTenant (t , & suite , tenantID )
998+ subscriptionNode = NewNode (t , & suite , graph .AsProperties (graph.PropertyMap {
999+ common .Name : "TestSubscription" ,
1000+ common .ObjectID : integration .RandomObjectID (t ),
1001+ graphAzure .TenantID : tenantID ,
1002+ }), graphAzure .Entity , graphAzure .Subscription )
1003+ appNode = NewAzureApplication (t , & suite , "App" , integration .RandomObjectID (t ), tenantID )
1004+ )
1005+
1006+ // tenant --Contains--> subscription --Contains--> app
1007+ NewRelationship (t , & suite , tenantNode , subscriptionNode , graphAzure .Contains )
1008+ NewRelationship (t , & suite , subscriptionNode , appNode , graphAzure .Contains )
1009+
1010+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1011+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1012+ require .NoError (t , err )
1013+
1014+ nodeIDs := paths .AllNodes ().IDs ()
1015+ require .Len (t , nodeIDs , 3 )
1016+ require .Contains (t , nodeIDs , tenantNode .ID )
1017+ require .Contains (t , nodeIDs , subscriptionNode .ID )
1018+ require .Contains (t , nodeIDs , appNode .ID )
1019+
1020+ return nil
1021+ })
1022+ require .NoError (t , err )
1023+ }
1024+
1025+ // TestFetchEntityDescendentPaths_NoTerminalsReturnsEmpty verifies that an empty path set is
1026+ // returned when no nodes of the requested kind exist within the tenant.
1027+ func TestFetchEntityDescendentPaths_NoTerminalsReturnsEmpty (t * testing.T ) {
1028+ t .Parallel ()
1029+
1030+ suite := setupIntegrationTestSuite (t )
1031+ defer teardownIntegrationTestSuite (t , & suite )
1032+
1033+ var (
1034+ tenantID = integration .RandomObjectID (t )
1035+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1036+ )
1037+
1038+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1039+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1040+ require .NoError (t , err )
1041+ require .Equal (t , 0 , paths .Len ())
1042+
1043+ return nil
1044+ })
1045+ require .NoError (t , err )
1046+ }
1047+
1048+ // TestFetchEntityDescendentPaths_SharedIntermediateNode verifies that two terminal nodes sharing
1049+ // an intermediate container node both produce complete paths reaching root. Each traversal
1050+ // independently reaches root through the shared intermediate without being truncated.
1051+ func TestFetchEntityDescendentPaths_SharedIntermediateNode (t * testing.T ) {
1052+ t .Parallel ()
1053+
1054+ suite := setupIntegrationTestSuite (t )
1055+ defer teardownIntegrationTestSuite (t , & suite )
1056+
1057+ var (
1058+ tenantID = integration .RandomObjectID (t )
1059+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1060+ subscriptionNode = NewNode (t , & suite , graph .AsProperties (graph.PropertyMap {
1061+ common .Name : "SharedSubscription" ,
1062+ common .ObjectID : integration .RandomObjectID (t ),
1063+ graphAzure .TenantID : tenantID ,
1064+ }), graphAzure .Entity , graphAzure .Subscription )
1065+ app1 = NewAzureApplication (t , & suite , "App1" , integration .RandomObjectID (t ), tenantID )
1066+ app2 = NewAzureApplication (t , & suite , "App2" , integration .RandomObjectID (t ), tenantID )
1067+ )
1068+
1069+ // tenant --Contains--> subscription --Contains--> app1
1070+ // --Contains--> app2
1071+ NewRelationship (t , & suite , tenantNode , subscriptionNode , graphAzure .Contains )
1072+ NewRelationship (t , & suite , subscriptionNode , app1 , graphAzure .Contains )
1073+ NewRelationship (t , & suite , subscriptionNode , app2 , graphAzure .Contains )
1074+
1075+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1076+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1077+ require .NoError (t , err )
1078+
1079+ nodeIDs := paths .AllNodes ().IDs ()
1080+ require .Contains (t , nodeIDs , tenantNode .ID )
1081+ require .Contains (t , nodeIDs , subscriptionNode .ID )
1082+ require .Contains (t , nodeIDs , app1 .ID )
1083+ require .Contains (t , nodeIDs , app2 .ID )
1084+
1085+ return nil
1086+ })
1087+ require .NoError (t , err )
1088+ }
1089+
1090+ // TestFetchEntityDescendentPaths_TerminalNotConnectedToRoot verifies that a terminal node
1091+ // which exists in the same tenant but has no azure.Contains path leading back to root
1092+ // is excluded from the returned path set.
1093+ func TestFetchEntityDescendentPaths_TerminalNotConnectedToRoot (t * testing.T ) {
1094+ t .Parallel ()
1095+
1096+ suite := setupIntegrationTestSuite (t )
1097+ defer teardownIntegrationTestSuite (t , & suite )
1098+
1099+ var (
1100+ tenantID = integration .RandomObjectID (t )
1101+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1102+ connectedApp = NewAzureApplication (t , & suite , "ConnectedApp" , integration .RandomObjectID (t ), tenantID )
1103+ disconnectedApp = NewAzureApplication (t , & suite , "DisconnectedApp" , integration .RandomObjectID (t ), tenantID )
1104+ )
1105+
1106+ // Only connectedApp has a Contains edge to root; disconnectedApp shares the tenantID
1107+ // but has no path leading back to tenantNode.
1108+ NewRelationship (t , & suite , tenantNode , connectedApp , graphAzure .Contains )
1109+
1110+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1111+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1112+ require .NoError (t , err )
1113+
1114+ nodeIDs := paths .AllNodes ().IDs ()
1115+ require .Contains (t , nodeIDs , tenantNode .ID )
1116+ require .Contains (t , nodeIDs , connectedApp .ID )
1117+ require .NotContains (t , nodeIDs , disconnectedApp .ID )
1118+
1119+ return nil
1120+ })
1121+ require .NoError (t , err )
1122+ }
1123+
1124+ // TestListEntityDescendents_NestedManagementGroupToSubscription verifies that a Subscription
1125+ // nested under one or more intermediate ManagementGroups is returned when listing the
1126+ // DescendentSubscriptions of an ancestor ManagementGroup.
1127+ func TestListEntityDescendents_NestedManagementGroupToSubscription (t * testing.T ) {
1128+ t .Parallel ()
1129+
1130+ suite := setupIntegrationTestSuite (t )
1131+ defer teardownIntegrationTestSuite (t , & suite )
1132+
1133+ var (
1134+ tenantID = integration .RandomObjectID (t )
1135+ mgRootObjectID = integration .RandomObjectID (t )
1136+ mgChildObjectID = integration .RandomObjectID (t )
1137+ subObjectID = integration .RandomObjectID (t )
1138+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1139+ mgRootNode = NewNode (t , & suite , graph .AsProperties (graph.PropertyMap {
1140+ common .Name : "MGRoot" ,
1141+ common .ObjectID : mgRootObjectID ,
1142+ graphAzure .TenantID : tenantID ,
1143+ }), graphAzure .Entity , graphAzure .ManagementGroup )
1144+ mgChildNode = NewNode (t , & suite , graph .AsProperties (graph.PropertyMap {
1145+ common .Name : "MGChild" ,
1146+ common .ObjectID : mgChildObjectID ,
1147+ graphAzure .TenantID : tenantID ,
1148+ }), graphAzure .Entity , graphAzure .ManagementGroup )
1149+ nestedSubNode = NewNode (t , & suite , graph .AsProperties (graph.PropertyMap {
1150+ common .Name : "NestedSubscription" ,
1151+ common .ObjectID : subObjectID ,
1152+ graphAzure .TenantID : tenantID ,
1153+ }), graphAzure .Entity , graphAzure .Subscription )
1154+ )
1155+
1156+ // tenant --Contains--> mgRoot --Contains--> mgChild --Contains--> nestedSub
1157+ NewRelationship (t , & suite , tenantNode , mgRootNode , graphAzure .Contains )
1158+ NewRelationship (t , & suite , mgRootNode , mgChildNode , graphAzure .Contains )
1159+ NewRelationship (t , & suite , mgChildNode , nestedSubNode , graphAzure .Contains )
1160+
1161+ nodes , err := azure .ListEntityDescendents (
1162+ suite .Context , suite .GraphDB ,
1163+ azure .RelatedEntityTypeDescendentSubscriptions , graphAzure .ManagementGroup ,
1164+ mgRootObjectID , 0 , 0 ,
1165+ )
1166+ require .NoError (t , err )
1167+
1168+ require .Equal (t , 1 , len (nodes ), "expected nested subscription to be returned as a descendent of mgRoot" )
1169+ require .Contains (t , nodes .IDs (), nestedSubNode .ID )
1170+ }
0 commit comments