Skip to content

Commit d25105c

Browse files
committed
test: add MaaS API key tests
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
1 parent cd77bc1 commit d25105c

2 files changed

Lines changed: 269 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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+
create_api_key,
9+
get_api_key,
10+
list_api_keys,
11+
revoke_api_key,
12+
)
13+
from utilities.general import generate_random_name
14+
15+
LOGGER = get_logger(name=__name__)
16+
17+
18+
@pytest.mark.usefixtures(
19+
"maas_subscription_controller_enabled_latest",
20+
"maas_gateway_api",
21+
"maas_api_gateway_reachable",
22+
)
23+
class TestAPIKeyCRUD:
24+
"""Tests for MaaS API key lifecycle: create, list, and revoke."""
25+
26+
@pytest.mark.tier1
27+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
28+
def test_create_api_key(
29+
self,
30+
request_session_http: requests.Session,
31+
base_url: str,
32+
ocp_token_for_actor: str,
33+
) -> None:
34+
"""Verify API key creation and show-once behavior."""
35+
36+
key_name = f"e2e-crud-create-{generate_random_name()}"
37+
38+
_, body = create_api_key(
39+
base_url=base_url,
40+
ocp_user_token=ocp_token_for_actor,
41+
request_session_http=request_session_http,
42+
api_key_name=key_name,
43+
)
44+
45+
assert "id" in body, f"Expected 'id' in create response, got: {body}"
46+
assert "key" in body, f"Expected 'key' in create response, got: {body}"
47+
assert "name" in body, f"Expected 'name' in create response, got: {body}"
48+
49+
key = body["key"]
50+
assert key.startswith("sk-oai-"), f"Expected 'sk-oai-' prefix, got: {key[:20]}"
51+
assert len(key) > len("sk-oai-"), "Key body after prefix must not be empty"
52+
53+
LOGGER.info(f"[create] Created key id={body['id']}, prefix={key[:15]}...")
54+
55+
get_resp, get_body = get_api_key(
56+
request_session_http=request_session_http,
57+
base_url=base_url,
58+
key_id=body["id"],
59+
ocp_user_token=ocp_token_for_actor,
60+
)
61+
assert get_resp.status_code == 200, (
62+
f"Expected 200 on GET /v1/api-keys/{body['id']}, got {get_resp.status_code}: {get_resp.text[:200]}"
63+
)
64+
assert "key" not in get_body, (
65+
"Plaintext key must not be returned by GET after creation (show-once pattern)"
66+
)
67+
68+
@pytest.mark.tier1
69+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
70+
def test_list_api_keys(
71+
self,
72+
request_session_http: requests.Session,
73+
base_url: str,
74+
ocp_token_for_actor: str,
75+
) -> None:
76+
"""Verify active API keys are listed and pagination works."""
77+
78+
key1_name = f"e2e-crud-list-1-{generate_random_name()}"
79+
key2_name = f"e2e-crud-list-2-{generate_random_name()}"
80+
81+
_, key1_body = create_api_key(
82+
base_url=base_url,
83+
ocp_user_token=ocp_token_for_actor,
84+
request_session_http=request_session_http,
85+
api_key_name=key1_name,
86+
)
87+
_, key2_body = create_api_key(
88+
base_url=base_url,
89+
ocp_user_token=ocp_token_for_actor,
90+
request_session_http=request_session_http,
91+
api_key_name=key2_name,
92+
)
93+
key1_id = key1_body["id"]
94+
key2_id = key2_body["id"]
95+
96+
list_resp, list_body = list_api_keys(
97+
request_session_http=request_session_http,
98+
base_url=base_url,
99+
ocp_user_token=ocp_token_for_actor,
100+
filters={"status": ["active"]},
101+
sort={"by": "created_at", "order": "desc"},
102+
pagination={"limit": 50, "offset": 0},
103+
)
104+
assert list_resp.status_code == 200, (
105+
f"Expected 200 on POST /v1/api-keys/search, got {list_resp.status_code}: {list_resp.text[:200]}"
106+
)
107+
108+
items: list[dict] = list_body.get("items") or list_body.get("data") or []
109+
assert len(items) >= 2, f"Expected at least 2 active keys, got {len(items)}"
110+
111+
key_ids = [item["id"] for item in items]
112+
assert key1_id in key_ids, f"key1 id={key1_id} not found in listed keys: {key_ids}"
113+
assert key2_id in key_ids, f"key2 id={key2_id} not found in listed keys: {key_ids}"
114+
115+
for item in items:
116+
assert "key" not in item, f"Plaintext key must not appear in any list item: {item}"
117+
118+
LOGGER.info(f"[list] Found {len(items)} active keys")
119+
120+
page_resp, page_body = list_api_keys(
121+
request_session_http=request_session_http,
122+
base_url=base_url,
123+
ocp_user_token=ocp_token_for_actor,
124+
filters={"status": ["active"]},
125+
sort={"by": "created_at", "order": "desc"},
126+
pagination={"limit": 1, "offset": 0},
127+
)
128+
assert page_resp.status_code == 200, (
129+
f"Expected 200 on paginated search, got {page_resp.status_code}: {page_resp.text[:200]}"
130+
)
131+
paged_items: list[dict] = page_body.get("items") or page_body.get("data") or []
132+
assert len(paged_items) <= 1, f"Expected at most 1 item with limit=1, got {len(paged_items)}"
133+
LOGGER.info(f"[list] Pagination limit=1 returned {len(paged_items)} item(s)")
134+
135+
@pytest.mark.tier1
136+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
137+
def test_revoke_api_key(
138+
self,
139+
request_session_http: requests.Session,
140+
base_url: str,
141+
ocp_token_for_actor: str,
142+
) -> None:
143+
"""Verify an API key can be revoked and remains revoked on GET."""
144+
145+
key_name = f"e2e-crud-revoke-{generate_random_name()}"
146+
147+
_, body = create_api_key(
148+
base_url=base_url,
149+
ocp_user_token=ocp_token_for_actor,
150+
request_session_http=request_session_http,
151+
api_key_name=key_name,
152+
)
153+
key_id = body["id"]
154+
155+
revoke_resp, revoke_body = revoke_api_key(
156+
request_session_http=request_session_http,
157+
base_url=base_url,
158+
key_id=key_id,
159+
ocp_user_token=ocp_token_for_actor,
160+
)
161+
assert revoke_resp.status_code == 200, (
162+
f"Expected 200 on DELETE /v1/api-keys/{key_id}, got {revoke_resp.status_code}: {revoke_resp.text[:200]}"
163+
)
164+
assert revoke_body.get("status") == "revoked", (
165+
f"Expected status='revoked' in DELETE response, got: {revoke_body}"
166+
)
167+
168+
get_resp, get_body = get_api_key(
169+
request_session_http=request_session_http,
170+
base_url=base_url,
171+
key_id=key_id,
172+
ocp_user_token=ocp_token_for_actor,
173+
)
174+
assert get_resp.status_code == 200, (
175+
f"Expected 200 on GET after revoke, got {get_resp.status_code}: {get_resp.text[:200]}"
176+
)
177+
assert get_body.get("status") == "revoked", (
178+
f"Expected status='revoked' on GET after revoke, got: {get_body}"
179+
)
180+
LOGGER.info(f"[revoke] Key {key_id} confirmed revoked")

tests/model_serving/maas_billing/maas_subscription/utils.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,95 @@ def create_api_key(
219219
return response, parsed_body
220220

221221

222+
def get_api_key(
223+
request_session_http: requests.Session,
224+
base_url: str,
225+
key_id: str,
226+
ocp_user_token: str,
227+
request_timeout_seconds: int = 60,
228+
) -> tuple[Response, dict[str, Any]]:
229+
"""
230+
Fetch a single API key by ID via MaaS API (GET /v1/api-keys/{id}).
231+
"""
232+
url = f"{base_url}/v1/api-keys/{key_id}"
233+
response = request_session_http.get(
234+
url=url,
235+
headers={"Authorization": f"Bearer {ocp_user_token}"},
236+
timeout=request_timeout_seconds,
237+
)
238+
LOGGER.info(f"get_api_key: url={url} key_id={key_id} status={response.status_code}")
239+
try:
240+
parsed_body: dict[str, Any] = json.loads(response.text)
241+
except json.JSONDecodeError as error:
242+
raise AssertionError(
243+
f"get_api_key returned non-JSON response: status={response.status_code} body={response.text[:200]}"
244+
) from error
245+
return response, parsed_body
246+
247+
248+
def list_api_keys(
249+
request_session_http: requests.Session,
250+
base_url: str,
251+
ocp_user_token: str,
252+
filters: dict[str, Any] | None = None,
253+
sort: dict[str, Any] | None = None,
254+
pagination: dict[str, Any] | None = None,
255+
request_timeout_seconds: int = 60,
256+
) -> tuple[Response, dict[str, Any]]:
257+
"""
258+
Search/list API keys via MaaS API (POST /v1/api-keys/search).
259+
"""
260+
url = f"{base_url}/v1/api-keys/search"
261+
payload: dict[str, Any] = {}
262+
if filters is not None:
263+
payload["filters"] = filters
264+
if sort is not None:
265+
payload["sort"] = sort
266+
if pagination is not None:
267+
payload["pagination"] = pagination
268+
269+
response = request_session_http.post(
270+
url=url,
271+
headers={"Authorization": f"Bearer {ocp_user_token}"},
272+
json=payload,
273+
timeout=request_timeout_seconds,
274+
)
275+
LOGGER.info(f"list_api_keys: url={url} status={response.status_code} items_count=pending_parse")
276+
try:
277+
parsed_body: dict[str, Any] = json.loads(response.text)
278+
except json.JSONDecodeError as error:
279+
raise AssertionError(
280+
f"list_api_keys returned non-JSON response: status={response.status_code} body={response.text[:200]}"
281+
) from error
282+
return response, parsed_body
283+
284+
285+
def revoke_api_key(
286+
request_session_http: requests.Session,
287+
base_url: str,
288+
key_id: str,
289+
ocp_user_token: str,
290+
request_timeout_seconds: int = 60,
291+
) -> tuple[Response, dict[str, Any]]:
292+
"""
293+
Revoke an API key via MaaS API (DELETE /v1/api-keys/{id}).
294+
"""
295+
url = f"{base_url}/v1/api-keys/{key_id}"
296+
response = request_session_http.delete(
297+
url=url,
298+
headers={"Authorization": f"Bearer {ocp_user_token}"},
299+
timeout=request_timeout_seconds,
300+
)
301+
LOGGER.info(f"revoke_api_key: url={url} key_id={key_id} status={response.status_code}")
302+
try:
303+
parsed_body: dict[str, Any] = json.loads(response.text)
304+
except json.JSONDecodeError as error:
305+
raise AssertionError(
306+
f"revoke_api_key returned non-JSON response: status={response.status_code} body={response.text[:200]}"
307+
) from error
308+
return response, parsed_body
309+
310+
222311
def get_maas_postgres_labels() -> dict[str, str]:
223312
return {
224313
"app": "postgres",

0 commit comments

Comments
 (0)