@@ -1528,6 +1528,86 @@ def test_metrics_resource_counts_via_rest(fastapi_test_app):
15281528 assert isinstance (per_project ["demo_project" ], dict )
15291529
15301530
1531+ def test_metrics_resource_counts_with_permission_errors (fastapi_test_app ):
1532+ """
1533+ Test that /metrics/resource_counts returns 200 with zero counts for
1534+ resource types that raise FeastPermissionError, instead of failing
1535+ the entire request. This simulates the scenario where access is
1536+ restricted for some resource types via GroupBasedPolicy,
1537+ NamespaceBasedPolicy, or CombinedGroupNamespacePolicy.
1538+ """
1539+ from unittest .mock import patch
1540+
1541+ from feast .errors import FeastPermissionError
1542+
1543+ original_get = fastapi_test_app .get
1544+
1545+ # First, get the baseline counts to know which resources have data
1546+ baseline = original_get ("/metrics/resource_counts?project=demo_project" ).json ()
1547+ baseline_counts = baseline ["counts" ]
1548+
1549+ # Patch grpc_call to raise FeastPermissionError for entities and data sources
1550+ # while allowing other resource types to succeed
1551+ original_grpc_call = None
1552+
1553+ def grpc_call_entities_denied (handler_fn , request ):
1554+ handler_name = getattr (handler_fn , "__name__" , "" )
1555+ if handler_name in ("ListEntities" , "ListDataSources" ):
1556+ raise FeastPermissionError (f"Permission denied for { handler_name } " )
1557+ return original_grpc_call (handler_fn , request )
1558+
1559+ with patch ("feast.api.registry.rest.metrics.grpc_call" ) as mock_grpc_call :
1560+ import feast .api .registry .rest .metrics as metrics_module
1561+
1562+ original_grpc_call = metrics_module .__dict__ .get ("grpc_call" )
1563+ # Restore actual import since patch replaces it
1564+ from feast .api .registry .rest .rest_utils import grpc_call as real_grpc_call
1565+
1566+ original_grpc_call = real_grpc_call
1567+ mock_grpc_call .side_effect = grpc_call_entities_denied
1568+
1569+ response = fastapi_test_app .get ("/metrics/resource_counts?project=demo_project" )
1570+
1571+ assert response .status_code == 200 , (
1572+ f"Expected 200 but got { response .status_code } : { response .text } "
1573+ )
1574+ data = response .json ()
1575+ assert "counts" in data
1576+ counts = data ["counts" ]
1577+
1578+ # Denied resource types should have 0 counts
1579+ assert counts ["entities" ] == 0
1580+ assert counts ["dataSources" ] == 0
1581+
1582+ # Permitted resource types should still have their original counts
1583+ assert counts ["featureViews" ] == baseline_counts ["featureViews" ]
1584+ assert counts ["featureServices" ] == baseline_counts ["featureServices" ]
1585+ assert counts ["features" ] == baseline_counts ["features" ]
1586+ assert counts ["savedDatasets" ] == baseline_counts ["savedDatasets" ]
1587+
1588+ # Now test with ALL resource types denied
1589+ def grpc_call_all_denied (handler_fn , request ):
1590+ handler_name = getattr (handler_fn , "__name__" , "" )
1591+ if handler_name .startswith ("List" ) and handler_name != "ListProjects" :
1592+ raise FeastPermissionError (f"Permission denied for { handler_name } " )
1593+ return real_grpc_call (handler_fn , request )
1594+
1595+ with patch ("feast.api.registry.rest.metrics.grpc_call" ) as mock_grpc_call :
1596+ mock_grpc_call .side_effect = grpc_call_all_denied
1597+
1598+ response = fastapi_test_app .get ("/metrics/resource_counts?project=demo_project" )
1599+
1600+ assert response .status_code == 200
1601+ data = response .json ()
1602+ counts = data ["counts" ]
1603+
1604+ # All counts should be 0 when all resource types are denied
1605+ for resource_type , count in counts .items ():
1606+ assert count == 0 , (
1607+ f"Expected 0 for { resource_type } when permission denied, got { count } "
1608+ )
1609+
1610+
15311611def test_feature_views_all_types_and_resource_counts_match (fastapi_test_app ):
15321612 """
15331613 Test that verifies:
0 commit comments