Skip to content

Commit 3ef50a9

Browse files
authored
Merge branch 'main' into label_asset
2 parents c36084a + f471c14 commit 3ef50a9

File tree

6 files changed

+325
-6
lines changed

6 files changed

+325
-6
lines changed

Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ RUN curl -sSL "https://github.com/fullstorydev/grpcurl/releases/download/v1.9.2/
2323
&& mv grpcurl /usr/bin/grpcurl
2424

2525
# Install cosign
26-
RUN curl -sSL "https://github.com/sigstore/cosign/releases/download/v2.4.2/cosign-linux-amd64" --output /usr/bin/cosign \
27-
&& chmod +x /usr/bin/cosign
26+
COPY --from=quay.io/securesign/cli-cosign@sha256:a8289d488491991d454a32784de19476f2c984917eb7a33b4544e55512f2747c /usr/local/bin/cosign /usr/bin/cosign
2827

2928
RUN useradd -ms /bin/bash $USER
3029
USER $USER

docs/GETTING_STARTED.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ To run tests with admin client only, pass `--tc=use_unprivileged_client:False` t
138138
### jira integration
139139

140140
To skip running tests which have open bugs, [pytest_jira](https://github.com/rhevm-qe-automation/pytest_jira) plugin is used.
141-
To run tests with jira integration, you need to set `PYTEST_JIRA_URL`, `PYTEST_JIRA_USERNAME` and `PYTEST_JIRA_PASSWORD` environment variables.
141+
To run tests with jira integration, you need to set `PYTEST_JIRA_URL` and `PYTEST_JIRA_TOKEN` environment variables.
142142
To make a test with jira marker, add: `@pytest.mark.jira(jira_id="RHOAIENG-0000", run=False)` to the test.
143143

144144
### Running containerized tests

tests/model_serving/maas_billing/maas_subscription/conftest.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
create_maas_subscription,
2929
get_maas_postgres_resources,
3030
patch_llmisvc_with_maas_router_and_tiers,
31+
revoke_api_key,
3132
wait_for_postgres_connection_log,
3233
wait_for_postgres_deployment_ready,
3334
)
@@ -620,3 +621,88 @@ def free_actor_premium_subscription(
620621
f"on premium model {maas_model_tinyllama_premium.name}"
621622
)
622623
yield sub_for_free_actor
624+
625+
626+
@pytest.fixture(scope="function")
627+
def two_active_api_key_ids(
628+
request_session_http: requests.Session,
629+
base_url: str,
630+
ocp_token_for_actor: str,
631+
) -> Generator[list[str], Any, Any]:
632+
"""
633+
Create two active API keys and return their IDs for list tests.
634+
"""
635+
ids = [
636+
create_api_key(
637+
base_url=base_url,
638+
ocp_user_token=ocp_token_for_actor,
639+
request_session_http=request_session_http,
640+
api_key_name=f"e2e-fixture-list-{i}-{generate_random_name()}",
641+
)[1]["id"]
642+
for i in range(1, 3)
643+
]
644+
LOGGER.info(f"two_active_api_key_ids: created keys {ids}")
645+
yield ids
646+
for key_id in ids:
647+
LOGGER.info(f"Fixture teardown: revoking key {key_id}")
648+
revoke_api_key(
649+
request_session_http=request_session_http,
650+
base_url=base_url,
651+
key_id=key_id,
652+
ocp_user_token=ocp_token_for_actor,
653+
)
654+
655+
656+
@pytest.fixture(scope="function")
657+
def active_api_key_id(
658+
request_session_http: requests.Session,
659+
base_url: str,
660+
ocp_token_for_actor: str,
661+
) -> Generator[str, Any, Any]:
662+
"""
663+
Create a single active API key and return its ID for revoke tests.
664+
"""
665+
key_name = f"e2e-fixture-key-{generate_random_name()}"
666+
_, body = create_api_key(
667+
base_url=base_url,
668+
ocp_user_token=ocp_token_for_actor,
669+
request_session_http=request_session_http,
670+
api_key_name=key_name,
671+
)
672+
LOGGER.info(f"active_api_key_id: created key id={body['id']}")
673+
yield body["id"]
674+
LOGGER.info(f"Fixture teardown: revoking key {body['id']}")
675+
revoke_api_key(
676+
request_session_http=request_session_http,
677+
base_url=base_url,
678+
key_id=body["id"],
679+
ocp_user_token=ocp_token_for_actor,
680+
)
681+
682+
683+
@pytest.fixture(scope="function")
684+
def revoked_api_key_id(
685+
request_session_http: requests.Session,
686+
base_url: str,
687+
ocp_token_for_actor: str,
688+
active_api_key_id: str,
689+
) -> str:
690+
"""
691+
Revoke the active API key and return its ID.
692+
693+
Asserts the DELETE response confirms status='revoked'.
694+
Used as a precondition fixture for tests that verify revoked state persists.
695+
"""
696+
revoke_resp, revoke_body = revoke_api_key(
697+
request_session_http=request_session_http,
698+
base_url=base_url,
699+
key_id=active_api_key_id,
700+
ocp_user_token=ocp_token_for_actor,
701+
)
702+
assert revoke_resp.status_code == 200, (
703+
f"Expected 200 on DELETE /v1/api-keys/{active_api_key_id}, "
704+
f"got {revoke_resp.status_code}: {revoke_resp.text[:200]}"
705+
)
706+
assert revoke_body.get("status") == "revoked", f"Expected status='revoked' in DELETE response, got: {revoke_body}"
707+
LOGGER.info(f"revoked_api_key_id: revoked key id={active_api_key_id}")
708+
return active_api_key_id
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
)
12+
from utilities.general import generate_random_name
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 TestAPIKeyCRUD:
23+
"""Tests for MaaS API key lifecycle: create, list, and revoke."""
24+
25+
@pytest.mark.tier1
26+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
27+
def test_create_api_key(
28+
self,
29+
request_session_http: requests.Session,
30+
base_url: str,
31+
ocp_token_for_actor: str,
32+
) -> None:
33+
"""Verify API key creation and show-once behavior."""
34+
35+
key_name = f"e2e-crud-create-{generate_random_name()}"
36+
37+
_, body = create_api_key(
38+
base_url=base_url,
39+
ocp_user_token=ocp_token_for_actor,
40+
request_session_http=request_session_http,
41+
api_key_name=key_name,
42+
)
43+
44+
for field in ("id", "key", "name"):
45+
assert field in body, f"Expected '{field}' field in create response"
46+
47+
key = body["key"]
48+
assert key.startswith("sk-oai-"), "Expected key to start with 'sk-oai-' prefix"
49+
assert len(key) > len("sk-oai-"), "Key body after prefix must not be empty"
50+
51+
LOGGER.info(f"[create] Created key id={body['id']}, key_prefix=sk-oai-***")
52+
53+
get_resp, get_body = get_api_key(
54+
request_session_http=request_session_http,
55+
base_url=base_url,
56+
key_id=body["id"],
57+
ocp_user_token=ocp_token_for_actor,
58+
)
59+
assert get_resp.status_code == 200, (
60+
f"Expected 200 on GET /v1/api-keys/{body['id']}, got {get_resp.status_code}: {get_resp.text[:200]}"
61+
)
62+
assert "key" not in get_body, "Plaintext key must not be returned by GET after creation (show-once pattern)"
63+
64+
@pytest.mark.tier1
65+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
66+
def test_list_api_keys(
67+
self,
68+
request_session_http: requests.Session,
69+
base_url: str,
70+
ocp_token_for_actor: str,
71+
two_active_api_key_ids: list[str],
72+
) -> None:
73+
"""Verify active API keys are listed and no plaintext key is exposed."""
74+
75+
list_resp, list_body = list_api_keys(
76+
request_session_http=request_session_http,
77+
base_url=base_url,
78+
ocp_user_token=ocp_token_for_actor,
79+
filters={"status": ["active"]},
80+
sort={"by": "created_at", "order": "desc"},
81+
pagination={"limit": 50, "offset": 0},
82+
)
83+
assert list_resp.status_code == 200, (
84+
f"Expected 200 on POST /v1/api-keys/search, got {list_resp.status_code}: {list_resp.text[:200]}"
85+
)
86+
87+
items: list[dict] = list_body.get("items") or list_body.get("data") or []
88+
assert len(items) >= 2, f"Expected at least 2 active keys, got {len(items)}"
89+
90+
key_ids = [item["id"] for item in items]
91+
for created_id in two_active_api_key_ids:
92+
assert created_id in key_ids, f"Created key id={created_id} not found in listed keys"
93+
94+
for item in items:
95+
assert "key" not in item, f"Plaintext key must not appear in any list item: {item}"
96+
97+
LOGGER.info(f"[list] Found {len(items)} active keys")
98+
99+
@pytest.mark.tier1
100+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
101+
def test_list_api_keys_pagination(
102+
self,
103+
request_session_http: requests.Session,
104+
base_url: str,
105+
ocp_token_for_actor: str,
106+
) -> None:
107+
"""Verify that the search endpoint respects the pagination limit."""
108+
109+
page_resp, page_body = list_api_keys(
110+
request_session_http=request_session_http,
111+
base_url=base_url,
112+
ocp_user_token=ocp_token_for_actor,
113+
filters={"status": ["active"]},
114+
sort={"by": "created_at", "order": "desc"},
115+
pagination={"limit": 1, "offset": 0},
116+
)
117+
assert page_resp.status_code == 200, (
118+
f"Expected 200 on paginated search, got {page_resp.status_code}: {page_resp.text[:200]}"
119+
)
120+
paged_items: list[dict] = page_body.get("items") or page_body.get("data") or []
121+
assert len(paged_items) <= 1, f"Expected at most 1 item with limit=1, got {len(paged_items)}"
122+
LOGGER.info(f"[list] Pagination limit=1 returned {len(paged_items)} item(s)")
123+
124+
@pytest.mark.tier1
125+
@pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
126+
def test_revoke_api_key(
127+
self,
128+
request_session_http: requests.Session,
129+
base_url: str,
130+
ocp_token_for_actor: str,
131+
revoked_api_key_id: str,
132+
) -> None:
133+
"""Verify a revoked API key shows status='revoked' on subsequent GET."""
134+
135+
get_resp, get_body = get_api_key(
136+
request_session_http=request_session_http,
137+
base_url=base_url,
138+
key_id=revoked_api_key_id,
139+
ocp_user_token=ocp_token_for_actor,
140+
)
141+
assert get_resp.status_code == 200, (
142+
f"Expected 200 on GET after revoke, got {get_resp.status_code}: {get_resp.text[:200]}"
143+
)
144+
assert get_body.get("status") == "revoked", f"Expected status='revoked' on GET after revoke, got: {get_body}"
145+
LOGGER.info(f"[revoke] Key {revoked_api_key_id} confirmed revoked")

tests/model_serving/maas_billing/maas_subscription/utils.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Generator, Sequence
55
from contextlib import contextmanager
66
from typing import Any
7-
from urllib.parse import urlparse
7+
from urllib.parse import quote, urlparse
88

99
import pytest
1010
import requests
@@ -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/{quote(key_id, safe='')}"
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/{quote(key_id, safe='')}"
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",

utilities/jira.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def get_jira_connection() -> JIRA:
2323
2424
"""
2525
return JIRA(
26-
basic_auth=(os.getenv("PYTEST_JIRA_USERNAME"), os.getenv("PYTEST_JIRA_PASSWORD")),
27-
options={"server": os.getenv("PYTEST_JIRA_URL")},
26+
server=os.getenv("PYTEST_JIRA_URL"),
27+
basic_auth=(os.getenv("PYTEST_JIRA_USERNAME"), os.getenv("PYTEST_JIRA_TOKEN")),
2828
)
2929

3030

0 commit comments

Comments
 (0)