Skip to content

Commit ccc4497

Browse files
authored
Merge branch 'main' into enhance-vector-store-dataset
2 parents f3e93d2 + 9ef3f9d commit ccc4497

File tree

3 files changed

+217
-2
lines changed

3 files changed

+217
-2
lines changed

tests/model_serving/maas_billing/maas_subscription/conftest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,3 +809,19 @@ def revoked_api_key_id(
809809
assert revoke_body.get("status") == "revoked", f"Expected status='revoked' in DELETE response, got: {revoke_body}"
810810
LOGGER.info(f"revoked_api_key_id: revoked key id={active_api_key_id}")
811811
return active_api_key_id
812+
813+
814+
@pytest.fixture(scope="function")
815+
def short_expiration_api_key_id(
816+
request_session_http: requests.Session,
817+
base_url: str,
818+
ocp_token_for_actor: str,
819+
) -> Generator[str, Any, Any]:
820+
"""Create an API key with 1-hour expiration, yield its ID, and revoke on teardown."""
821+
yield from create_and_yield_api_key_id(
822+
request_session_http=request_session_http,
823+
base_url=base_url,
824+
ocp_user_token=ocp_token_for_actor,
825+
key_name_prefix="e2e-exp-short",
826+
expires_in="1h",
827+
)
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
import requests
5+
6+
from tests.model_serving.maas_billing.maas_subscription.utils import (
7+
assert_api_key_created_ok,
8+
assert_api_key_get_ok,
9+
create_api_key,
10+
get_api_key,
11+
revoke_api_key,
12+
)
13+
from utilities.general import generate_random_name
14+
from utilities.opendatahub_logger import get_logger
15+
16+
LOGGER = get_logger(name=__name__)
17+
18+
MAAS_API_KEY_MAX_EXPIRATION_DAYS = 30
19+
20+
21+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
22+
@pytest.mark.usefixtures(
23+
"maas_subscription_controller_enabled_latest",
24+
"maas_gateway_api",
25+
"maas_api_gateway_reachable",
26+
)
27+
class TestAPIKeyExpiration:
28+
"""Tests for API key expiration policy enforcement."""
29+
30+
@pytest.mark.tier1
31+
def test_create_key_within_expiration_limit(
32+
self,
33+
request_session_http: requests.Session,
34+
base_url: str,
35+
ocp_token_for_actor: str,
36+
) -> None:
37+
"""Verify creating an API key with expiration below the limit succeeds."""
38+
expires_in_hours = max((MAAS_API_KEY_MAX_EXPIRATION_DAYS // 2) * 24, 24)
39+
key_name = f"e2e-exp-within-{generate_random_name()}"
40+
41+
create_resp, create_body = create_api_key(
42+
base_url=base_url,
43+
ocp_user_token=ocp_token_for_actor,
44+
request_session_http=request_session_http,
45+
api_key_name=key_name,
46+
expires_in=f"{expires_in_hours}h",
47+
raise_on_error=False,
48+
)
49+
assert_api_key_created_ok(resp=create_resp, body=create_body, required_fields=("key", "expiresAt"))
50+
LOGGER.info(
51+
f"[expiration] Created key within limit: expires_in={expires_in_hours}h, "
52+
f"expiresAt={create_body.get('expiresAt')}"
53+
)
54+
revoke_api_key(
55+
request_session_http=request_session_http,
56+
base_url=base_url,
57+
key_id=create_body["id"],
58+
ocp_user_token=ocp_token_for_actor,
59+
)
60+
61+
@pytest.mark.tier1
62+
def test_create_key_at_expiration_limit(
63+
self,
64+
request_session_http: requests.Session,
65+
base_url: str,
66+
ocp_token_for_actor: str,
67+
) -> None:
68+
"""Verify creating an API key with expiration exactly at the limit succeeds."""
69+
expires_in_hours = MAAS_API_KEY_MAX_EXPIRATION_DAYS * 24
70+
key_name = f"e2e-exp-at-limit-{generate_random_name()}"
71+
72+
create_resp, create_body = create_api_key(
73+
base_url=base_url,
74+
ocp_user_token=ocp_token_for_actor,
75+
request_session_http=request_session_http,
76+
api_key_name=key_name,
77+
expires_in=f"{expires_in_hours}h",
78+
raise_on_error=False,
79+
)
80+
assert_api_key_created_ok(resp=create_resp, body=create_body, required_fields=("key", "expiresAt"))
81+
LOGGER.info(
82+
f"[expiration] Created key at limit: expires_in={expires_in_hours}h "
83+
f"({MAAS_API_KEY_MAX_EXPIRATION_DAYS} days)"
84+
)
85+
revoke_api_key(
86+
request_session_http=request_session_http,
87+
base_url=base_url,
88+
key_id=create_body["id"],
89+
ocp_user_token=ocp_token_for_actor,
90+
)
91+
92+
@pytest.mark.tier1
93+
def test_create_key_exceeds_expiration_limit(
94+
self,
95+
request_session_http: requests.Session,
96+
base_url: str,
97+
ocp_token_for_actor: str,
98+
) -> None:
99+
"""Verify creating an API key with expiration beyond the limit returns 400."""
100+
exceeds_days = MAAS_API_KEY_MAX_EXPIRATION_DAYS * 2
101+
key_name = f"e2e-exp-exceeds-{generate_random_name()}"
102+
103+
create_resp, _ = create_api_key(
104+
base_url=base_url,
105+
ocp_user_token=ocp_token_for_actor,
106+
request_session_http=request_session_http,
107+
api_key_name=key_name,
108+
expires_in=f"{exceeds_days * 24}h",
109+
raise_on_error=False,
110+
)
111+
assert create_resp.status_code == 400, (
112+
f"Expected 400 for expiration exceeding limit "
113+
f"({exceeds_days} days > {MAAS_API_KEY_MAX_EXPIRATION_DAYS} days limit), "
114+
f"got {create_resp.status_code}: {create_resp.text[:200]}"
115+
)
116+
error_text = create_resp.text.lower()
117+
assert "exceed" in error_text or "maximum" in error_text, (
118+
f"Expected error body to mention 'exceed' or 'maximum': {create_resp.text[:200]}"
119+
)
120+
LOGGER.info(
121+
f"[expiration] Correctly rejected key: {exceeds_days} days > {MAAS_API_KEY_MAX_EXPIRATION_DAYS} days limit"
122+
)
123+
124+
@pytest.mark.tier1
125+
def test_create_key_without_expiration(
126+
self,
127+
request_session_http: requests.Session,
128+
base_url: str,
129+
ocp_token_for_actor: str,
130+
active_api_key_id: str,
131+
) -> None:
132+
"""Verify a key created without an expiration field has no expiresAt value."""
133+
get_resp, get_body = get_api_key(
134+
request_session_http=request_session_http,
135+
base_url=base_url,
136+
key_id=active_api_key_id,
137+
ocp_user_token=ocp_token_for_actor,
138+
)
139+
assert_api_key_get_ok(resp=get_resp, body=get_body, key_id=active_api_key_id)
140+
expires_at = get_body.get("expiresAt")
141+
assert expires_at is None, f"Expected no 'expiresAt' for key created without expiration, got: {expires_at!r}"
142+
LOGGER.info(f"[expiration] Key without expiration field: expiresAt={expires_at!r}")
143+
144+
@pytest.mark.tier2
145+
def test_create_key_with_short_expiration(
146+
self,
147+
request_session_http: requests.Session,
148+
base_url: str,
149+
ocp_token_for_actor: str,
150+
short_expiration_api_key_id: str,
151+
) -> None:
152+
"""Verify a key created with a 1-hour expiration has a non-null expirationDate value."""
153+
get_resp, get_body = get_api_key(
154+
request_session_http=request_session_http,
155+
base_url=base_url,
156+
key_id=short_expiration_api_key_id,
157+
ocp_user_token=ocp_token_for_actor,
158+
)
159+
assert_api_key_get_ok(resp=get_resp, body=get_body, key_id=short_expiration_api_key_id)
160+
assert get_body.get("expirationDate"), (
161+
f"Expected non-null 'expirationDate' for 1h key, got: {get_body.get('expirationDate')!r}"
162+
)
163+
LOGGER.info(f"[expiration] 1h key expirationDate={get_body['expirationDate']}")

tests/model_serving/maas_billing/maas_subscription/utils.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,28 +185,42 @@ def create_api_key(
185185
request_session_http: requests.Session,
186186
api_key_name: str,
187187
request_timeout_seconds: int = 60,
188+
expires_in: str | None = None,
189+
raise_on_error: bool = True,
188190
) -> tuple[Response, dict[str, Any]]:
189191
"""
190192
Create an API key via MaaS API and return (response, parsed_body).
191193
192194
Uses ocp_user_token for auth against maas-api.
193195
Expects plaintext key in body["key"] (sk-...).
196+
197+
Args:
198+
expires_in: Optional expiration duration string (e.g. "24h", "720h").
199+
When None, no expiresIn field is sent and the key does not expire.
200+
raise_on_error: When True (default), raises AssertionError for non-200/201
201+
responses. Set to False when testing error cases (e.g. 400 rejection).
194202
"""
195203
api_keys_url = f"{base_url}/v1/api-keys"
196204

205+
payload: dict[str, Any] = {"name": api_key_name}
206+
if expires_in is not None:
207+
payload["expiresIn"] = expires_in
208+
197209
response = request_session_http.post(
198210
url=api_keys_url,
199211
headers={
200212
"Authorization": f"Bearer {ocp_user_token}",
201213
"Content-Type": "application/json",
202214
},
203-
json={"name": api_key_name},
215+
json=payload,
204216
timeout=request_timeout_seconds,
205217
)
206218

207219
LOGGER.info(f"create_api_key: url={api_keys_url} status={response.status_code}")
208220
if response.status_code not in (200, 201):
209-
raise AssertionError(f"api-key create failed: status={response.status_code}")
221+
if raise_on_error:
222+
raise AssertionError(f"api-key create failed: status={response.status_code}")
223+
return response, {}
210224

211225
try:
212226
parsed_body: dict[str, Any] = json.loads(response.text)
@@ -326,6 +340,7 @@ def create_and_yield_api_key_id(
326340
base_url: str,
327341
ocp_user_token: str,
328342
key_name_prefix: str,
343+
expires_in: str | None = None,
329344
) -> Generator[str]:
330345
"""Create an API key, yield its ID, and revoke it on teardown."""
331346
key_name = f"{key_name_prefix}-{generate_random_name()}"
@@ -334,6 +349,7 @@ def create_and_yield_api_key_id(
334349
ocp_user_token=ocp_user_token,
335350
request_session_http=request_session_http,
336351
api_key_name=key_name,
352+
expires_in=expires_in,
337353
)
338354
LOGGER.info(f"create_and_yield_api_key_id: created key id={body['id']} name={key_name}")
339355
yield body["id"]
@@ -431,6 +447,26 @@ def assert_bulk_revoke_success(
431447
return revoked_count
432448

433449

450+
def assert_api_key_created_ok(
451+
resp: Response,
452+
body: dict[str, Any],
453+
required_fields: tuple[str, ...] = ("key",),
454+
) -> None:
455+
"""Assert an API key creation response has a success status and expected fields."""
456+
assert resp.status_code in (200, 201), (
457+
f"Expected 200/201 for API key creation, got {resp.status_code}: {resp.text[:200]}"
458+
)
459+
for field in required_fields:
460+
assert field in body, f"Response must contain '{field}'"
461+
462+
463+
def assert_api_key_get_ok(resp: Response, body: dict[str, Any], key_id: str) -> None:
464+
"""Assert a GET /v1/api-keys/{id} response has status 200."""
465+
assert resp.status_code == 200, (
466+
f"Expected 200 on GET /v1/api-keys/{key_id}, got {resp.status_code}: {resp.text[:200]}"
467+
)
468+
469+
434470
def get_maas_postgres_labels() -> dict[str, str]:
435471
return {
436472
"app": "postgres",

0 commit comments

Comments
 (0)