|
2 | 2 | from typing import Self |
3 | 3 | from simple_logger.logger import get_logger |
4 | 4 |
|
5 | | -from tests.model_registry.model_catalog.utils import execute_get_command, validate_filter_options_structure |
| 5 | +from tests.model_registry.model_catalog.utils import ( |
| 6 | + execute_get_command, |
| 7 | + validate_filter_options_structure, |
| 8 | + parse_psql_array_agg_output, |
| 9 | + get_postgres_pod_in_namespace, |
| 10 | + compare_filter_options_with_database, |
| 11 | +) |
| 12 | +from tests.model_registry.model_catalog.constants import FILTER_OPTIONS_DB_QUERY, API_EXCLUDED_FILTER_FIELDS |
6 | 13 | from tests.model_registry.utils import get_rest_headers |
7 | 14 | from utilities.user_utils import UserTestSession |
8 | 15 |
|
|
13 | 20 | ] |
14 | 21 |
|
15 | 22 |
|
16 | | -@pytest.mark.parametrize( |
17 | | - "user_token_for_api_calls,", |
18 | | - [ |
19 | | - pytest.param( |
20 | | - {}, |
21 | | - id="test_filter_options_admin_user", |
22 | | - ), |
23 | | - pytest.param( |
24 | | - {"user_type": "test"}, |
25 | | - id="test_filter_options_non_admin_user", |
26 | | - ), |
27 | | - pytest.param( |
28 | | - {"user_type": "sa_user"}, |
29 | | - id="test_filter_options_service_account", |
30 | | - ), |
31 | | - ], |
32 | | - indirect=["user_token_for_api_calls"], |
33 | | -) |
34 | 23 | class TestFilterOptionsEndpoint: |
35 | 24 | """ |
36 | 25 | Test class for validating the models/filter_options endpoint |
37 | 26 | RHOAIENG-36696 |
38 | 27 | """ |
39 | 28 |
|
| 29 | + @pytest.mark.parametrize( |
| 30 | + "user_token_for_api_calls,", |
| 31 | + [ |
| 32 | + pytest.param( |
| 33 | + {}, |
| 34 | + id="test_filter_options_admin_user", |
| 35 | + ), |
| 36 | + pytest.param( |
| 37 | + {"user_type": "test"}, |
| 38 | + id="test_filter_options_non_admin_user", |
| 39 | + ), |
| 40 | + pytest.param( |
| 41 | + {"user_type": "sa_user"}, |
| 42 | + id="test_filter_options_service_account", |
| 43 | + ), |
| 44 | + ], |
| 45 | + indirect=["user_token_for_api_calls"], |
| 46 | + ) |
40 | 47 | def test_filter_options_endpoint_validation( |
41 | 48 | self: Self, |
42 | 49 | model_catalog_rest_url: list[str], |
@@ -74,48 +81,66 @@ def test_filter_options_endpoint_validation( |
74 | 81 | LOGGER.info(f"Found {len(filters)} filter properties: {list(filters.keys())}") |
75 | 82 | LOGGER.info("All filter options validation passed successfully") |
76 | 83 |
|
77 | | - @pytest.mark.skip(reason="TODO: Implement after investigating backend DB queries") |
| 84 | + # Cannot use non-admin user for this test as it cannot list the pods in the namespace |
| 85 | + @pytest.mark.parametrize( |
| 86 | + "user_token_for_api_calls,", |
| 87 | + [ |
| 88 | + pytest.param( |
| 89 | + {}, |
| 90 | + id="test_filter_options_admin_user", |
| 91 | + ), |
| 92 | + pytest.param( |
| 93 | + {"user_type": "sa_user"}, |
| 94 | + id="test_filter_options_service_account", |
| 95 | + ), |
| 96 | + ], |
| 97 | + indirect=["user_token_for_api_calls"], |
| 98 | + ) |
78 | 99 | def test_comprehensive_coverage_against_database( |
79 | 100 | self: Self, |
80 | 101 | model_catalog_rest_url: list[str], |
81 | 102 | user_token_for_api_calls: str, |
82 | | - test_idp_user: UserTestSession, |
| 103 | + model_registry_namespace: str, |
83 | 104 | ): |
84 | 105 | """ |
85 | | - STUBBED: Validate filter options are comprehensive across all sources/models in DB. |
| 106 | + Validate filter options are comprehensive across all sources/models in DB. |
86 | 107 | Acceptance Criteria: The returned options are comprehensive and not limited to a |
87 | 108 | subset of models or a single source. |
88 | 109 |
|
89 | | - TODO IMPLEMENTATION PLAN: |
90 | | - 1. Investigate backend endpoint logic: |
91 | | - - Find the source code for /models/filter_options endpoint in kubeflow/model-registry |
92 | | - - Understand what DB tables it queries (likely model/artifact tables) |
93 | | - - Identify the exact SQL queries used to build filter values |
94 | | - - Determine database schema and column names |
95 | | -
|
96 | | - 2. Replicate queries via pod shell: |
97 | | - - Use get_model_catalog_pod() to access catalog pod |
98 | | - - Execute psql commands via pod.execute() |
99 | | - - Query same tables/columns the endpoint uses |
100 | | - - Extract all distinct values for string properties: SELECT DISTINCT license FROM models; |
101 | | - - Extract min/max ranges for numeric properties: SELECT MIN(metric), MAX(metric) FROM models; |
102 | | -
|
103 | | - 3. Compare results: |
104 | | - - API response filter values should match DB query results exactly |
105 | | - - Ensure no values are missing (comprehensive coverage) |
106 | | - - Validate across all sources, not just one |
107 | | -
|
108 | | - 4. DB Access Pattern Example: |
109 | | - catalog_pod = get_model_catalog_pod(client, namespace)[0] |
110 | | - result = catalog_pod.execute( |
111 | | - command=["psql", "-U", "catalog_user", "-d", "catalog_db", "-c", "SELECT DISTINCT license FROM models;"], |
112 | | - container="catalog" |
113 | | - ) |
114 | | -
|
115 | | - 5. Implementation considerations: |
116 | | - - Handle different data types (strings vs arrays like tasks) |
117 | | - - Parse psql output correctly |
118 | | - - Handle null/empty values |
119 | | - - Ensure database connection credentials are available |
| 110 | + This test executes the exact same SQL query the API uses and compares results |
| 111 | + to catch any discrepancies between database content and API response. |
| 112 | +
|
| 113 | + Expected failure because of RHOAIENG-37069 |
120 | 114 | """ |
121 | | - pytest.skip("TODO: Implement comprehensive coverage validation after backend investigation") |
| 115 | + api_url = f"{model_catalog_rest_url[0]}models/filter_options" |
| 116 | + LOGGER.info(f"Testing comprehensive database coverage for: {api_url}") |
| 117 | + |
| 118 | + api_response = execute_get_command( |
| 119 | + url=api_url, |
| 120 | + headers=get_rest_headers(token=user_token_for_api_calls), |
| 121 | + ) |
| 122 | + |
| 123 | + api_filters = api_response["filters"] |
| 124 | + LOGGER.info(f"API returned {len(api_filters)} filter properties: {list(api_filters.keys())}") |
| 125 | + |
| 126 | + postgres_pod = get_postgres_pod_in_namespace(namespace=model_registry_namespace) |
| 127 | + LOGGER.info(f"Using PostgreSQL pod: {postgres_pod.name}") |
| 128 | + |
| 129 | + db_result = postgres_pod.execute( |
| 130 | + command=["psql", "-U", "catalog_user", "-d", "model_catalog", "-c", FILTER_OPTIONS_DB_QUERY], |
| 131 | + container="postgresql", |
| 132 | + ) |
| 133 | + |
| 134 | + db_properties = parse_psql_array_agg_output(psql_output=db_result) |
| 135 | + LOGGER.info(f"Raw database query returned {len(db_properties)} properties: {list(db_properties.keys())}") |
| 136 | + |
| 137 | + is_valid, comparison_errors = compare_filter_options_with_database( |
| 138 | + api_filters=api_filters, db_properties=db_properties, excluded_fields=API_EXCLUDED_FILTER_FIELDS |
| 139 | + ) |
| 140 | + |
| 141 | + if not is_valid: |
| 142 | + failure_msg = "Filter options API response does not match database content" |
| 143 | + failure_msg += "\nDetailed comparison errors:\n" + "\n".join(comparison_errors) |
| 144 | + assert False, failure_msg |
| 145 | + |
| 146 | + LOGGER.info("Comprehensive database coverage validation passed - API matches database exactly") |
0 commit comments