Skip to content

Commit 65fe5fd

Browse files
authored
Merge branch 'main' into logging
2 parents 6a7a443 + 2690541 commit 65fe5fd

File tree

7 files changed

+350
-16
lines changed

7 files changed

+350
-16
lines changed

tests/model_registry/mcp_servers/config/conftest.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
MCP_CATALOG_INVALID_SOURCE,
1111
MCP_CATALOG_SOURCE,
1212
MCP_CATALOG_SOURCE2,
13+
MCP_CATALOG_SOURCE3,
1314
MCP_CATALOG_SOURCE_ID,
1415
MCP_CATALOG_SOURCE_NAME,
1516
MCP_SERVERS_YAML,
1617
MCP_SERVERS_YAML2,
18+
MCP_SERVERS_YAML3,
1719
MCP_SERVERS_YAML_CATALOG_PATH,
1820
)
1921
from tests.model_registry.utils import (
@@ -63,6 +65,46 @@ def mcp_multi_source_configmap_patch(
6365
)
6466

6567

68+
@pytest.fixture(scope="class")
69+
def mcp_source_label_configmap_patch(
70+
admin_client: DynamicClient,
71+
model_registry_namespace: str,
72+
mcp_catalog_rest_urls: list[str],
73+
model_registry_rest_headers: dict[str, str],
74+
) -> Generator[None]:
75+
"""
76+
Class-scoped fixture that patches the model-catalog-sources ConfigMap
77+
with three MCP catalog sources: two labeled and one unlabeled.
78+
Used for sourceLabel filtering tests (TC-API-036 to TC-API-039).
79+
"""
80+
catalog_config_map, current_data = get_mcp_catalog_sources(
81+
admin_client=admin_client, model_registry_namespace=model_registry_namespace
82+
)
83+
if "mcp_catalogs" not in current_data:
84+
current_data["mcp_catalogs"] = []
85+
current_data["mcp_catalogs"].extend([MCP_CATALOG_SOURCE, MCP_CATALOG_SOURCE2, MCP_CATALOG_SOURCE3])
86+
87+
patches = {
88+
"data": {
89+
"sources.yaml": yaml.dump(current_data, default_flow_style=False),
90+
"mcp-servers.yaml": MCP_SERVERS_YAML,
91+
"mcp-servers-2.yaml": MCP_SERVERS_YAML2,
92+
"mcp-servers-3.yaml": MCP_SERVERS_YAML3,
93+
}
94+
}
95+
96+
with ResourceEditor(patches={catalog_config_map: patches}):
97+
wait_for_model_catalog_pod_ready_after_deletion(
98+
client=admin_client, model_registry_namespace=model_registry_namespace
99+
)
100+
wait_for_mcp_catalog_api(url=mcp_catalog_rest_urls[0], headers=model_registry_rest_headers)
101+
yield
102+
103+
wait_for_model_catalog_pod_ready_after_deletion(
104+
client=admin_client, model_registry_namespace=model_registry_namespace
105+
)
106+
107+
66108
@pytest.fixture(scope="class")
67109
def mcp_invalid_yaml_configmap_patch(
68110
request: pytest.FixtureRequest,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Self
2+
3+
import pytest
4+
from simple_logger.logger import get_logger
5+
6+
from tests.model_registry.mcp_servers.constants import (
7+
MCP_CATALOG_SOURCE2_NAME,
8+
MCP_CATALOG_SOURCE_NAME,
9+
)
10+
from tests.model_registry.utils import execute_get_command
11+
12+
LOGGER = get_logger(name=__name__)
13+
14+
15+
@pytest.mark.usefixtures("mcp_source_label_configmap_patch")
16+
class TestMCPServerSourceLabel:
17+
"""Tests for MCP server sourceLabel filtering (TC-API-036 to TC-API-039)."""
18+
19+
@pytest.mark.smoke
20+
def test_mcp_server_source_label(
21+
self: Self,
22+
mcp_catalog_rest_urls: list[str],
23+
model_registry_rest_headers: dict[str, str],
24+
):
25+
"""
26+
Validate MCP server filtering by source label (TC-API-036, TC-API-038, TC-API-039).
27+
"""
28+
source1_size = execute_get_command(
29+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
30+
headers=model_registry_rest_headers,
31+
params={"sourceLabel": MCP_CATALOG_SOURCE_NAME},
32+
)["size"]
33+
source2_size = execute_get_command(
34+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
35+
headers=model_registry_rest_headers,
36+
params={"sourceLabel": MCP_CATALOG_SOURCE2_NAME},
37+
)["size"]
38+
null_label_size = execute_get_command(
39+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
40+
headers=model_registry_rest_headers,
41+
params={"sourceLabel": "null"},
42+
)["size"]
43+
no_filtered_size = execute_get_command(
44+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
45+
headers=model_registry_rest_headers,
46+
)["size"]
47+
both_labeled_size = execute_get_command(
48+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
49+
headers=model_registry_rest_headers,
50+
params={"sourceLabel": f"{MCP_CATALOG_SOURCE_NAME},{MCP_CATALOG_SOURCE2_NAME}"},
51+
)["size"]
52+
LOGGER.info(f"no_filtered_size: {no_filtered_size}")
53+
assert no_filtered_size > 0
54+
assert null_label_size >= 0
55+
assert source1_size + source2_size == both_labeled_size
56+
assert no_filtered_size == both_labeled_size + null_label_size
57+
58+
@pytest.mark.tier3
59+
def test_mcp_server_invalid_source_label(
60+
self: Self,
61+
mcp_catalog_rest_urls: list[str],
62+
model_registry_rest_headers: dict[str, str],
63+
):
64+
"""
65+
Validate MCP server filtering by invalid source label (TC-API-037).
66+
"""
67+
invalid_size = execute_get_command(
68+
url=f"{mcp_catalog_rest_urls[0]}mcp_servers",
69+
headers=model_registry_rest_headers,
70+
params={"sourceLabel": "invalid"},
71+
)["size"]
72+
73+
assert invalid_size == 0

tests/model_registry/mcp_servers/constants.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,36 @@
184184
"calculator": MCP_CATALOG_SOURCE_ID,
185185
"code-reviewer": MCP_CATALOG_SOURCE2_ID,
186186
}
187+
188+
# Source 3: unlabeled source (no labels) for sourceLabel=null testing
189+
MCP_CATALOG_SOURCE3_ID: str = "test_mcp_servers_unlabeled"
190+
MCP_CATALOG_SOURCE3_NAME: str = "Test MCP Servers Unlabeled"
191+
MCP_SERVERS_YAML3_CATALOG_PATH: str = "/data/user-sources/mcp-servers-3.yaml"
192+
193+
MCP_SERVERS_YAML3: str = """\
194+
mcp_servers:
195+
- name: database-connector
196+
description: "Database connection MCP server"
197+
provider: "Data Tools"
198+
version: "1.0.0"
199+
license: "MIT"
200+
tags:
201+
- database
202+
- sql
203+
tools:
204+
- name: execute_query
205+
description: "Execute a database query"
206+
"""
207+
208+
MCP_CATALOG_SOURCE3: dict = {
209+
"name": MCP_CATALOG_SOURCE3_NAME,
210+
"id": MCP_CATALOG_SOURCE3_ID,
211+
"type": "yaml",
212+
"enabled": True,
213+
"properties": {"yamlCatalogPath": MCP_SERVERS_YAML3_CATALOG_PATH},
214+
}
215+
216+
EXPECTED_MCP_SOURCE3_SERVER_NAMES: set[str] = {"database-connector"}
217+
EXPECTED_ALL_MCP_SERVER_NAMES_WITH_UNLABELED: set[str] = (
218+
EXPECTED_ALL_MCP_SERVER_NAMES | EXPECTED_MCP_SOURCE3_SERVER_NAMES
219+
)

tests/model_registry/model_catalog/search/test_model_search.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_search_model_catalog_source_label(
4040
Validate search model catalog by source label
4141
"""
4242

43-
redhat_ai_filter_moldels_size = get_models_from_catalog_api(
43+
redhat_ai_filter_models_size = get_models_from_catalog_api(
4444
model_catalog_rest_url=model_catalog_rest_url,
4545
model_registry_rest_headers=model_registry_rest_headers,
4646
source_label=REDHAT_AI_CATALOG_NAME,
@@ -50,20 +50,24 @@ def test_search_model_catalog_source_label(
5050
model_registry_rest_headers=model_registry_rest_headers,
5151
source_label=REDHAT_AI_VALIDATED_UNESCAPED_CATALOG_NAME,
5252
)["size"]
53+
null_label_models_size = get_models_from_catalog_api(
54+
model_catalog_rest_url=model_catalog_rest_url,
55+
model_registry_rest_headers=model_registry_rest_headers,
56+
source_label="null",
57+
)["size"]
5358
no_filtered_models_size = get_models_from_catalog_api(
5459
model_catalog_rest_url=model_catalog_rest_url, model_registry_rest_headers=model_registry_rest_headers
5560
)["size"]
56-
both_filtered_models_size = get_models_from_catalog_api(
61+
both_labeled_models_size = get_models_from_catalog_api(
5762
model_catalog_rest_url=model_catalog_rest_url,
5863
model_registry_rest_headers=model_registry_rest_headers,
5964
source_label=f"{REDHAT_AI_VALIDATED_UNESCAPED_CATALOG_NAME},{REDHAT_AI_CATALOG_NAME}",
6065
)["size"]
6166
LOGGER.info(f"no_filtered_models_size: {no_filtered_models_size}")
6267
assert no_filtered_models_size > 0
63-
# no_filtered includes models from sources without labels (e.g. Other Models),
64-
# which cannot be queried via sourceLabel, so total >= labeled sum
65-
assert no_filtered_models_size >= both_filtered_models_size
66-
assert redhat_ai_filter_moldels_size + redhat_ai_validated_filter_models_size == both_filtered_models_size
68+
assert null_label_models_size >= 0
69+
assert redhat_ai_filter_models_size + redhat_ai_validated_filter_models_size == both_labeled_models_size
70+
assert no_filtered_models_size == both_labeled_models_size + null_label_models_size
6771

6872
@pytest.mark.tier3
6973
def test_search_model_catalog_invalid_source_label(
@@ -74,22 +78,13 @@ def test_search_model_catalog_invalid_source_label(
7478
"""
7579
Validate search model catalog by invalid source label
7680
"""
77-
78-
# "null" is a valid source label for sources without explicit labels (e.g. Other Models)
79-
null_size = get_models_from_catalog_api(
80-
model_catalog_rest_url=model_catalog_rest_url,
81-
model_registry_rest_headers=model_registry_rest_headers,
82-
source_label="null",
83-
)["size"]
84-
8581
invalid_size = get_models_from_catalog_api(
8682
model_catalog_rest_url=model_catalog_rest_url,
8783
model_registry_rest_headers=model_registry_rest_headers,
8884
source_label="invalid",
8985
)["size"]
9086

91-
assert null_size >= 0, f"Expected non-negative size for null source label, got {null_size}"
92-
assert invalid_size == 0, f"Expected 0 models for invalid source label, got {invalid_size}"
87+
assert invalid_size == 0
9388

9489
@pytest.mark.parametrize(
9590
"randomly_picked_model_from_catalog_api_by_source,source_filter",

tests/model_serving/maas_billing/maas_subscription/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,36 @@ def two_active_api_key_ids(
657657
)
658658

659659

660+
@pytest.fixture(scope="function")
661+
def three_active_api_key_ids(
662+
request_session_http: requests.Session,
663+
base_url: str,
664+
ocp_token_for_actor: str,
665+
) -> Generator[list[str], Any, Any]:
666+
"""Create three active API keys and yield their IDs for bulk-revoke tests."""
667+
key_ids = [
668+
create_api_key(
669+
base_url=base_url,
670+
ocp_user_token=ocp_token_for_actor,
671+
request_session_http=request_session_http,
672+
api_key_name=f"e2e-bulk-key-{index}-{generate_random_name()}",
673+
)[1]["id"]
674+
for index in range(1, 4)
675+
]
676+
LOGGER.info(f"three_active_api_key_ids: created keys {key_ids}")
677+
yield key_ids
678+
for key_id in key_ids:
679+
LOGGER.info(f"three_active_api_key_ids: teardown revoking key {key_id}")
680+
revoke_resp, _ = revoke_api_key(
681+
request_session_http=request_session_http,
682+
base_url=base_url,
683+
key_id=key_id,
684+
ocp_user_token=ocp_token_for_actor,
685+
)
686+
if revoke_resp.status_code not in (200, 404):
687+
raise AssertionError(f"Unexpected teardown status for key id={key_id}: {revoke_resp.status_code}")
688+
689+
660690
@pytest.fixture(scope="function")
661691
def active_api_key_id(
662692
request_session_http: requests.Session,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
import requests
5+
from simple_logger.logger import get_logger
6+
7+
from tests.model_serving.maas_billing.maas_subscription.utils import (
8+
assert_bulk_revoke_success,
9+
bulk_revoke_api_keys,
10+
get_api_key,
11+
resolve_api_key_username,
12+
)
13+
14+
LOGGER = get_logger(name=__name__)
15+
16+
17+
@pytest.mark.usefixtures(
18+
"maas_subscription_controller_enabled_latest",
19+
"maas_gateway_api",
20+
"maas_api_gateway_reachable",
21+
)
22+
class TestAPIKeyBulkOperations:
23+
"""Tests for MaaS API key bulk revoke operations."""
24+
25+
@pytest.mark.tier1
26+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
27+
def test_bulk_revoke_own_keys(
28+
self,
29+
request_session_http: requests.Session,
30+
base_url: str,
31+
ocp_token_for_actor: str,
32+
three_active_api_key_ids: list[str],
33+
) -> None:
34+
"""Verify a user can bulk revoke all their own active API keys."""
35+
username = resolve_api_key_username(
36+
request_session_http=request_session_http,
37+
base_url=base_url,
38+
key_id=three_active_api_key_ids[0],
39+
ocp_user_token=ocp_token_for_actor,
40+
)
41+
42+
revoked_count = assert_bulk_revoke_success(
43+
request_session_http=request_session_http,
44+
base_url=base_url,
45+
ocp_user_token=ocp_token_for_actor,
46+
username=username,
47+
min_revoked_count=3,
48+
)
49+
LOGGER.info(f"[bulk-revoke] User {username} bulk revoked {revoked_count} key(s)")
50+
51+
for key_id in three_active_api_key_ids:
52+
get_resp, get_body = get_api_key(
53+
request_session_http=request_session_http,
54+
base_url=base_url,
55+
key_id=key_id,
56+
ocp_user_token=ocp_token_for_actor,
57+
)
58+
assert get_resp.status_code == 200, (
59+
f"Expected 200 on GET /v1/api-keys/{key_id} after bulk-revoke, "
60+
f"got {get_resp.status_code}: {get_resp.text[:200]}"
61+
)
62+
assert get_body.get("status") == "revoked", (
63+
f"Expected key id={key_id} to have status='revoked', got: {get_body.get('status')}"
64+
)
65+
LOGGER.info(f"[bulk-revoke] All {len(three_active_api_key_ids)} key(s) confirmed revoked")
66+
67+
@pytest.mark.tier1
68+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
69+
def test_bulk_revoke_other_user_forbidden(
70+
self,
71+
request_session_http: requests.Session,
72+
base_url: str,
73+
ocp_token_for_actor: str,
74+
) -> None:
75+
"""Verify a non-admin user gets 403 when attempting to bulk revoke another user's keys."""
76+
bulk_resp, _ = bulk_revoke_api_keys(
77+
request_session_http=request_session_http,
78+
base_url=base_url,
79+
ocp_user_token=ocp_token_for_actor,
80+
username="someotheruser",
81+
)
82+
assert bulk_resp.status_code == 403, (
83+
f"Expected 403 (non-admin cannot bulk revoke other users), "
84+
f"got {bulk_resp.status_code}: {bulk_resp.text[:200]}"
85+
)
86+
LOGGER.info("[bulk-revoke] Non-admin correctly received 403 when attempting to bulk revoke another user's keys")
87+
88+
@pytest.mark.tier1
89+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
90+
def test_bulk_revoke_admin_can_revoke_any_user(
91+
self,
92+
request_session_http: requests.Session,
93+
base_url: str,
94+
active_api_key_id: str,
95+
free_user_username: str,
96+
admin_ocp_token: str,
97+
) -> None:
98+
"""Verify an admin can bulk revoke any user's active API keys."""
99+
revoked_count = assert_bulk_revoke_success(
100+
request_session_http=request_session_http,
101+
base_url=base_url,
102+
ocp_user_token=admin_ocp_token,
103+
username=free_user_username,
104+
min_revoked_count=1,
105+
)
106+
LOGGER.info(f"[bulk-revoke] Admin successfully revoked {revoked_count} key(s) for user {free_user_username}")

0 commit comments

Comments
 (0)