Skip to content

Commit b15d5f9

Browse files
SB159dbasunag
andauthored
test: add list subscriptions and list subscriptions for model tests (#1346)
* test: add list subscriptions and list subscriptions for model tests Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com> * address review comments Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com> * fix:address coderabit comment Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com> --------- Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com> Co-authored-by: Debarati Basu-Nag <dbasunag@redhat.com>
1 parent 774a83e commit b15d5f9

File tree

5 files changed

+256
-0
lines changed

5 files changed

+256
-0
lines changed

tests/model_serving/maas_billing/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,12 @@ def maas_headers_for_actor(maas_token_for_actor: str) -> dict:
555555
return build_maas_headers(token=maas_token_for_actor)
556556

557557

558+
@pytest.fixture(scope="class")
559+
def ocp_headers_for_actor(ocp_token_for_actor: str) -> dict[str, str]:
560+
"""Headers built from the OCP token for the current actor (admin/free/premium)."""
561+
return build_maas_headers(token=ocp_token_for_actor)
562+
563+
558564
@pytest.fixture(scope="class")
559565
def maas_models_response_for_actor(
560566
request_session_http: requests.Session,

tests/model_serving/maas_billing/maas_subscription/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ def maas_subscription_tinyllama_premium(
135135
yield maas_subscription_premium
136136

137137

138+
@pytest.fixture(scope="class")
139+
def models_url(base_url: str) -> str:
140+
"""GET /v1/models endpoint URL."""
141+
return f"{base_url}/v1/models"
142+
143+
138144
@pytest.fixture(scope="class")
139145
def model_url_tinyllama_free(
140146
maas_scheme: str,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
import requests
5+
import structlog
6+
from ocp_resources.maas_subscription import MaaSSubscription
7+
8+
from tests.model_serving.maas_billing.maas_subscription.utils import assert_subscription_info_schema
9+
10+
LOGGER = structlog.get_logger(name=__name__)
11+
12+
13+
@pytest.mark.usefixtures(
14+
"maas_unprivileged_model_namespace",
15+
"maas_subscription_controller_enabled_latest",
16+
"maas_gateway_api",
17+
"maas_api_gateway_reachable",
18+
"maas_model_tinyllama_free",
19+
"maas_auth_policy_tinyllama_free",
20+
"maas_subscription_tinyllama_free",
21+
)
22+
class TestListSubscriptions:
23+
"""Verify a user can list their subscriptions."""
24+
25+
@pytest.mark.tier1
26+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
27+
def test_authenticated_user_sees_accessible_subscriptions(
28+
self,
29+
request_session_http: requests.Session,
30+
base_url: str,
31+
ocp_headers_for_actor: dict[str, str],
32+
maas_subscription_tinyllama_free: MaaSSubscription,
33+
) -> None:
34+
"""Verify authenticated user gets their accessible subscriptions."""
35+
response = request_session_http.get(
36+
url=f"{base_url}/v1/subscriptions", headers=ocp_headers_for_actor, timeout=30
37+
)
38+
39+
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {(response.text or '')[:200]}"
40+
41+
subscriptions = response.json()
42+
assert isinstance(subscriptions, list), f"Expected array response, got {type(subscriptions).__name__}"
43+
assert len(subscriptions) >= 1, f"Expected at least 1 subscription, got {len(subscriptions)}"
44+
45+
for subscription in subscriptions:
46+
assert_subscription_info_schema(subscription=subscription)
47+
48+
subscription_ids = [subscription["subscription_id_header"] for subscription in subscriptions]
49+
assert maas_subscription_tinyllama_free.name in subscription_ids, (
50+
f"Expected '{maas_subscription_tinyllama_free.name}' in accessible subscriptions, got {subscription_ids}"
51+
)
52+
LOGGER.info(
53+
f"[subscriptions] GET /v1/subscriptions -> {len(subscriptions)} subscription(s): {subscription_ids}"
54+
)
55+
56+
@pytest.mark.tier1
57+
def test_unauthenticated_returns_401(
58+
self,
59+
request_session_http: requests.Session,
60+
base_url: str,
61+
) -> None:
62+
"""Verify request without auth header returns 401."""
63+
response = request_session_http.get(url=f"{base_url}/v1/subscriptions", timeout=30)
64+
65+
assert response.status_code == 401, f"Expected 401, got {response.status_code}: {(response.text or '')[:200]}"
66+
LOGGER.info(f"[subscriptions] GET /v1/subscriptions (no auth) -> {response.status_code}")
67+
68+
@pytest.mark.tier1
69+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
70+
def test_subscription_response_includes_model_refs_with_rate_limits(
71+
self,
72+
request_session_http: requests.Session,
73+
base_url: str,
74+
ocp_headers_for_actor: dict[str, str],
75+
maas_subscription_tinyllama_free: MaaSSubscription,
76+
) -> None:
77+
"""Verify subscription response includes model_refs with rate limit info."""
78+
response = request_session_http.get(
79+
url=f"{base_url}/v1/subscriptions", headers=ocp_headers_for_actor, timeout=30
80+
)
81+
82+
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {(response.text or '')[:200]}"
83+
84+
subscriptions = response.json()
85+
assert isinstance(subscriptions, list), f"Expected array response, got {type(subscriptions).__name__}"
86+
87+
target_subscription = next(
88+
(
89+
subscription
90+
for subscription in subscriptions
91+
if subscription["subscription_id_header"] == maas_subscription_tinyllama_free.name
92+
),
93+
None,
94+
)
95+
assert target_subscription is not None, (
96+
f"Subscription '{maas_subscription_tinyllama_free.name}' not found in "
97+
f"{[subscription['subscription_id_header'] for subscription in subscriptions]}"
98+
)
99+
100+
assert len(target_subscription["model_refs"]) >= 1, "Expected at least 1 model_ref"
101+
model_ref = target_subscription["model_refs"][0]
102+
assert isinstance(model_ref["name"], str), "model_ref name must be a string"
103+
104+
assert "token_rate_limits" in model_ref, "model_ref missing 'token_rate_limits'"
105+
assert len(model_ref["token_rate_limits"]) >= 1, "Expected at least 1 token_rate_limit"
106+
rate_limit = model_ref["token_rate_limits"][0]
107+
assert "limit" in rate_limit, "token_rate_limit missing 'limit'"
108+
assert "window" in rate_limit, "token_rate_limit missing 'window'"
109+
110+
LOGGER.info(
111+
f"[subscriptions] Subscription '{target_subscription['subscription_id_header']}' "
112+
f"model_refs: {target_subscription['model_refs']}"
113+
)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
import requests
5+
import structlog
6+
from ocp_resources.maas_model_ref import MaaSModelRef
7+
from ocp_resources.maas_subscription import MaaSSubscription
8+
9+
from tests.model_serving.maas_billing.maas_subscription.utils import assert_subscription_info_schema
10+
11+
LOGGER = structlog.get_logger(name=__name__)
12+
13+
14+
@pytest.mark.usefixtures(
15+
"maas_unprivileged_model_namespace",
16+
"maas_subscription_controller_enabled_latest",
17+
"maas_gateway_api",
18+
"maas_api_gateway_reachable",
19+
"maas_model_tinyllama_free",
20+
"maas_auth_policy_tinyllama_free",
21+
"maas_subscription_tinyllama_free",
22+
"maas_model_tinyllama_premium",
23+
"maas_auth_policy_tinyllama_premium",
24+
"maas_subscription_tinyllama_premium",
25+
)
26+
class TestListSubscriptionsForModel:
27+
"""Verify only subscriptions belonging to a given model are returned."""
28+
29+
@pytest.mark.tier1
30+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
31+
def test_returns_only_subscriptions_for_requested_model(
32+
self,
33+
request_session_http: requests.Session,
34+
base_url: str,
35+
ocp_headers_for_actor: dict[str, str],
36+
maas_model_tinyllama_free: MaaSModelRef,
37+
maas_subscription_tinyllama_free: MaaSSubscription,
38+
maas_subscription_tinyllama_premium: MaaSSubscription,
39+
) -> None:
40+
"""Verify endpoint returns only subscriptions referencing the requested model."""
41+
model_name = maas_model_tinyllama_free.name
42+
43+
response = request_session_http.get(
44+
url=f"{base_url}/v1/model/{model_name}/subscriptions", headers=ocp_headers_for_actor, timeout=30
45+
)
46+
47+
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {(response.text or '')[:200]}"
48+
49+
subscriptions = response.json()
50+
assert isinstance(subscriptions, list), f"Expected array response, got {type(subscriptions).__name__}"
51+
52+
subscription_ids = [subscription["subscription_id_header"] for subscription in subscriptions]
53+
54+
assert maas_subscription_tinyllama_free.name in subscription_ids, (
55+
f"Expected '{maas_subscription_tinyllama_free.name}' in results for "
56+
f"model '{model_name}', got {subscription_ids}"
57+
)
58+
assert maas_subscription_tinyllama_premium.name not in subscription_ids, (
59+
f"'{maas_subscription_tinyllama_premium.name}' should not appear in results "
60+
f"for model '{model_name}', got {subscription_ids}"
61+
)
62+
63+
for subscription in subscriptions:
64+
assert_subscription_info_schema(subscription=subscription)
65+
66+
LOGGER.info(
67+
f"[subscriptions] GET /v1/model/{model_name}/subscriptions "
68+
f"-> {len(subscriptions)} subscription(s): {subscription_ids}"
69+
)
70+
71+
@pytest.mark.tier1
72+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True)
73+
def test_unknown_model_returns_empty_list(
74+
self,
75+
request_session_http: requests.Session,
76+
base_url: str,
77+
ocp_headers_for_actor: dict[str, str],
78+
) -> None:
79+
"""Verify GET /v1/model/{unknown-model}/subscriptions returns 200 with an empty list."""
80+
unknown_model = "nonexistent-model-xyz"
81+
82+
response = request_session_http.get(
83+
url=f"{base_url}/v1/model/{unknown_model}/subscriptions", headers=ocp_headers_for_actor, timeout=30
84+
)
85+
86+
assert response.status_code == 200, f"Expected 200, got {response.status_code}: {(response.text or '')[:200]}"
87+
88+
subscriptions = response.json()
89+
assert isinstance(subscriptions, list), f"Expected array response, got {type(subscriptions).__name__}"
90+
assert len(subscriptions) == 0, (
91+
f"Expected empty list for unknown model, got {len(subscriptions)}: {subscriptions}"
92+
)
93+
LOGGER.info(f"[subscriptions] GET /v1/model/{unknown_model}/subscriptions -> [] (empty list)")
94+
95+
@pytest.mark.tier1
96+
def test_unauthenticated_returns_401(
97+
self,
98+
request_session_http: requests.Session,
99+
base_url: str,
100+
maas_model_tinyllama_free: MaaSModelRef,
101+
) -> None:
102+
"""Verify request without auth header returns 401."""
103+
model_name = maas_model_tinyllama_free.name
104+
105+
response = request_session_http.get(url=f"{base_url}/v1/model/{model_name}/subscriptions", timeout=30)
106+
107+
assert response.status_code == 401, f"Expected 401, got {response.status_code}: {(response.text or '')[:200]}"
108+
LOGGER.info(f"[subscriptions] GET /v1/model/{model_name}/subscriptions (no auth) -> {response.status_code}")

tests/model_serving/maas_billing/maas_subscription/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,26 @@ def wait_for_postgres_connection_log(
433433
return
434434

435435
raise TimeoutError(f"PostgreSQL pod in namespace {namespace} did not report accepting connections")
436+
437+
438+
def assert_subscription_info_schema(subscription: dict[str, Any]) -> None:
439+
"""Assert a SubscriptionInfo object has the expected structure and field types."""
440+
assert "subscription_id_header" in subscription, f"Missing subscription_id_header: {subscription}"
441+
assert isinstance(subscription["subscription_id_header"], str), "subscription_id_header must be string"
442+
assert "subscription_description" in subscription, f"Missing subscription_description: {subscription}"
443+
assert isinstance(subscription["subscription_description"], str), "subscription_description must be string"
444+
assert "priority" in subscription, f"Missing priority: {subscription}"
445+
assert isinstance(subscription["priority"], int), "priority must be integer"
446+
assert "model_refs" in subscription, f"Missing model_refs: {subscription}"
447+
assert isinstance(subscription["model_refs"], list), "model_refs must be a list"
448+
for model_ref in subscription["model_refs"]:
449+
assert "name" in model_ref, f"model_ref missing name: {model_ref}"
450+
assert isinstance(model_ref["name"], str), "model_ref name must be string"
451+
if "display_name" in subscription:
452+
assert isinstance(subscription["display_name"], str), "display_name must be string"
453+
if "organization_id" in subscription:
454+
assert isinstance(subscription["organization_id"], str), "organization_id must be string"
455+
if "cost_center" in subscription:
456+
assert isinstance(subscription["cost_center"], str), "cost_center must be string"
457+
if "labels" in subscription:
458+
assert isinstance(subscription["labels"], dict), "labels must be a dict"

0 commit comments

Comments
 (0)