@@ -949,3 +949,131 @@ 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_DifferentTenantExcluded verifies that nodes sharing the same kind
1026+ // but belonging to a different Azure tenant are not included in the path set.
1027+ func TestFetchEntityDescendentPaths_DifferentTenantExcluded (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+ otherTenantID = integration .RandomObjectID (t )
1036+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1037+ ownApp = NewAzureApplication (t , & suite , "OwnApp" , integration .RandomObjectID (t ), tenantID )
1038+ foreignApp = NewAzureApplication (t , & suite , "ForeignApp" , integration .RandomObjectID (t ), otherTenantID )
1039+ )
1040+
1041+ NewRelationship (t , & suite , tenantNode , ownApp , graphAzure .Contains )
1042+ // foreignApp is deliberately not connected; it belongs to a different tenant.
1043+
1044+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1045+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1046+ require .NoError (t , err )
1047+
1048+ nodeIDs := paths .AllNodes ().IDs ()
1049+ require .Contains (t , nodeIDs , tenantNode .ID )
1050+ require .Contains (t , nodeIDs , ownApp .ID )
1051+ require .NotContains (t , nodeIDs , foreignApp .ID )
1052+
1053+ return nil
1054+ })
1055+ require .NoError (t , err )
1056+ }
1057+
1058+ // TestFetchEntityDescendentPaths_NoTerminalsReturnsEmpty verifies that an empty path set is
1059+ // returned when no nodes of the requested kind exist within the tenant.
1060+ func TestFetchEntityDescendentPaths_NoTerminalsReturnsEmpty (t * testing.T ) {
1061+ t .Parallel ()
1062+
1063+ suite := setupIntegrationTestSuite (t )
1064+ defer teardownIntegrationTestSuite (t , & suite )
1065+
1066+ var (
1067+ tenantID = integration .RandomObjectID (t )
1068+ tenantNode = NewAzureTenant (t , & suite , tenantID )
1069+ )
1070+
1071+ err := suite .GraphDB .ReadTransaction (suite .Context , func (tx graph.Transaction ) error {
1072+ paths , err := azure .FetchEntityDescendentPaths (tx , tenantNode , graphAzure .App )
1073+ require .NoError (t , err )
1074+ require .Equal (t , 0 , paths .Len ())
1075+
1076+ return nil
1077+ })
1078+ require .NoError (t , err )
1079+ }
0 commit comments