Skip to content

test: add MaaS API key tests#1244

Merged
sheltoncyril merged 6 commits intoopendatahub-io:mainfrom
SB159:feature/api-key-crud-tests
Mar 18, 2026
Merged

test: add MaaS API key tests#1244
sheltoncyril merged 6 commits intoopendatahub-io:mainfrom
SB159:feature/api-key-crud-tests

Conversation

@SB159
Copy link
Copy Markdown
Contributor

@SB159 SB159 commented Mar 17, 2026

Pull Request

Summary

add MaaS API key tests

Related Issues

  • Fixes:
  • JIRA:

How it has been tested

  • Locally
  • Jenkins

Additional Requirements

  • If this PR introduces a new test image, did you create a PR to mirror it in disconnected environment?
  • If this PR introduces new marker(s)/adds a new component, was relevant ticket created to update relevant Jenkins job?

Summary by CodeRabbit

  • Tests
    • Added comprehensive API key lifecycle tests covering creation, retrieval, listing (with filtering, sorting and pagination), and revocation; verifies plaintext keys are shown only once and key prefix behavior.
    • Added fixtures to provide active and revoked API key preconditions and ensure cleanup.
    • Added test utilities to fetch, search/list, and revoke API keys to stabilize coverage.

@github-actions
Copy link
Copy Markdown

The following are automatically added/executed:

  • PR size label.
  • Run pre-commit
  • Run tox
  • Add PR author as the PR assignee
  • Build image based on the PR

Available user actions:

  • To mark a PR as WIP, add /wip in a comment. To remove it from the PR comment /wip cancel to the PR.
  • To block merging of a PR, add /hold in a comment. To un-block merging of PR comment /hold cancel.
  • To mark a PR as approved, add /lgtm in a comment. To remove, add /lgtm cancel.
    lgtm label removed on each new commit push.
  • To mark PR as verified comment /verified to the PR, to un-verify comment /verified cancel to the PR.
    verified label removed on each new commit push.
  • To Cherry-pick a merged PR /cherry-pick <target_branch_name> to the PR. If <target_branch_name> is valid,
    and the current PR is merged, a cherry-picked PR would be created and linked to the current PR.
  • To build and push image to quay, add /build-push-pr-image in a comment. This would create an image with tag
    pr-<pr_number> to quay repository. This image tag, however would be deleted on PR merge or close action.
Supported labels

{'/wip', '/verified', '/cherry-pick', '/hold', '/lgtm', '/build-push-pr-image'}

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 17, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a MaaS API key CRUD test suite, three pytest fixtures for key lifecycle, and three HTTP helper functions (get/list/revoke) using Bearer auth, URL encoding, and strict JSON parsing; tests cover create (show-once), list (filters/sort/pagination), and revoke flows.

Changes

Cohort / File(s) Summary
API Key Test Suite
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py
New TestAPIKeyCRUD with tests: test_create_api_key (create, assert returned id/key/name, confirm plaintext not returned on subsequent GET, key prefix check), test_list_api_keys (create two keys, list with active filter, desc sort, pagination assertions, ensure no plaintext in listings), test_revoke_api_key (revoke and assert status "revoked").
API Key Utility Helpers
tests/model_serving/maas_billing/maas_subscription/utils.py
Added get_api_key (GET /v1/api-keys/{id}), list_api_keys (POST /v1/api-keys/search with filters/sort/pagination), revoke_api_key (DELETE /v1/api-keys/{id}); use quote for URL encoding, Bearer header, default 60s timeout, strict JSON parsing and logging of URLs/status.
Test Fixtures
tests/model_serving/maas_billing/maas_subscription/conftest.py
Added fixtures: two_active_api_key_ids, active_api_key_id, revoked_api_key_id to create keys for tests and revoke them in teardown; rely on helper functions for lifecycle operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Actionable Issues

  • Avoid logging secrets: Helpers and tests log request URLs and response status; ensure plaintext API keys and tokens are never logged. Redact or omit key values from logs. (CWE-532: Exposure of Sensitive Information)
  • Prevent error-message leaks: Helpers assert/raise on JSON parse failures and may include raw response bodies in errors — remove raw body dumps from assertion messages to avoid leaking secrets or internal details. (CWE-209: Information Exposure Through an Error Message)
  • Protect Authorization material: Authorization tokens are constructed and used in headers — ensure tokens are not interpolated into URLs, logs, or exception messages and treat them as secrets (rotate/test-scoped). (CWE-613: Insufficient Session Expiration / Management; CWE-532)
  • Enforce HTTPS and cert validation in tests: Tests assume HTTP client config; ensure tests use HTTPS and do not disable TLS certificate verification to avoid MITM risks. (CWE-295: Improper Certificate Validation)
  • Ensure robust teardown on failures: Fixtures revoke keys in normal teardown, but test failures may leave keys active. Add finalizers/try/finally that guarantee revocation and consider short-lived test keys. (Operational risk / CWE-532)
  • Limit show-once assertion exposure: Tests assert plaintext is not returned on subsequent GETs — ensure any failure assertions do not print the plaintext key or full response content. (CWE-532)
🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. The Summary section provides minimal detail (one line repeating the title), and critical sections lack meaningful content. Related Issues remain unfilled with placeholders. Testing mentions local verification but omits specifics. Checklist items remain unchecked without explanation. Expand the Summary with what API key CRUD operations are tested and why. Complete the Related Issues section or explicitly state none exist. Detail what local testing verified. Address the Additional Requirements checklist items or explain why they don't apply.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding MaaS API key tests. It directly reflects the primary purpose of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (1)

27-27: Security coverage gap: CRUD tests run only with admin actor.

Current parametrization skips free/premium actor paths available in tests/model_serving/maas_billing/conftest.py:459-502, so authorization regressions can pass undetected.

As per coding guidelines, "**: REVIEW PRIORITIES: 1. Security vulnerabilities ... 2. Architectural issues and anti-patterns".

Also applies to: 67-67, 134-134

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py` at
line 27, The tests only parametrize ocp_token_for_actor as admin, leaving out
free/premium actor types and missing authorization paths; update each
pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}],
indirect=True) occurrence in
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (and the
other two occurrences noted) to include the free and premium actor variants
(e.g. pass a list of dicts with {"type":"admin"}, {"type":"free"},
{"type":"premium"}) so the fixture defined in
tests/model_serving/maas_billing/conftest.py will exercise admin/free/premium
authorization paths across the CRUD tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Line 53: The test currently logs secret API key material via LOGGER.info using
the key variable and key[:15]; remove any secret exposure by changing the log to
omit the key and only record non-sensitive identifiers (e.g., use
LOGGER.info(f"[create] Created key id={body['id']}") or log a masked form
produced by a helper like mask_key(key) that does not include raw key bytes),
update references to LOGGER, body['id'], and key accordingly, and ensure no
other tests print key or key[:...] anywhere.
- Around line 28-131: Tests create API keys with create_api_key (in
test_create_api_key and test_list_api_keys) but never remove them; wrap resource
creation in try/finally or use a fixture to always call the cleanup helper
(e.g., delete_api_key) with the returned id(s) and the same
request_session_http/base_url/ocp_user_token so keys are deactivated/removed
after assertions; ensure the delete_api_key call checks the response status
(200/204) and tolerate missing keys on cleanup, and apply the same pattern for
both test_create_api_key (delete the single created id) and test_list_api_keys
(delete key1_id and key2_id) to prevent test-state leakage.

In `@tests/model_serving/maas_billing/maas_subscription/utils.py`:
- Around line 232-233: The key_id is interpolated directly into the path in
get_api_key() and revoke_api_key(), allowing path-control characters to
manipulate endpoints; URL-encode or percent-encode key_id (e.g., via
urllib.parse.quote with safe='' or equivalent) before constructing the url
variable so characters like '/' and '?' are escaped, then use the encoded value
when calling request_session_http.get/post to prevent resource path traversal.

---

Nitpick comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Line 27: The tests only parametrize ocp_token_for_actor as admin, leaving out
free/premium actor types and missing authorization paths; update each
pytest.mark.parametrize("ocp_token_for_actor", [{"type": "admin"}],
indirect=True) occurrence in
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (and the
other two occurrences noted) to include the free and premium actor variants
(e.g. pass a list of dicts with {"type":"admin"}, {"type":"free"},
{"type":"premium"}) so the fixture defined in
tests/model_serving/maas_billing/conftest.py will exercise admin/free/premium
authorization paths across the CRUD tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 738df610-02bc-4161-ae81-0242f65786be

📥 Commits

Reviewing files that changed from the base of the PR and between 73cc4d5 and 000e0e5.

📒 Files selected for processing (2)
  • tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py
  • tests/model_serving/maas_billing/maas_subscription/utils.py

Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/utils.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (1)

38-65: ⚠️ Potential issue | 🟠 Major

Guarantee API key cleanup in finally to prevent cross-test leakage.

At Line 38-65 and Line 79-131, created keys are left active. This leaks persistent state and can cause quota-driven flakes across runs.

Remediation diff
     def test_create_api_key(
@@
-        _, body = create_api_key(
+        _, body = create_api_key(
             base_url=base_url,
             ocp_user_token=ocp_token_for_actor,
             request_session_http=request_session_http,
             api_key_name=key_name,
         )
+        key_id = body["id"]
+        try:
+            assert "id" in body, f"Expected 'id' in create response, got: {body}"
+            assert "key" in body, f"Expected 'key' in create response, got: {body}"
+            assert "name" in body, f"Expected 'name' in create response, got: {body}"
+
+            key = body["key"]
+            assert key.startswith("sk-oai-"), f"Expected 'sk-oai-' prefix, got: {key[:20]}"
+            assert len(key) > len("sk-oai-"), "Key body after prefix must not be empty"
+
+            get_resp, get_body = get_api_key(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                key_id=body["id"],
+                ocp_user_token=ocp_token_for_actor,
+            )
+            assert get_resp.status_code == 200, (
+                f"Expected 200 on GET /v1/api-keys/{body['id']}, got {get_resp.status_code}: {get_resp.text[:200]}"
+            )
+            assert "key" not in get_body, "Plaintext key must not be returned by GET after creation (show-once pattern)"
+        finally:
+            revoke_api_key(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                key_id=key_id,
+                ocp_user_token=ocp_token_for_actor,
+            )
@@
-        _, key1_body = create_api_key(
+        _, key1_body = create_api_key(
             base_url=base_url,
             ocp_user_token=ocp_token_for_actor,
             request_session_http=request_session_http,
             api_key_name=key1_name,
         )
@@
         key1_id = key1_body["id"]
         key2_id = key2_body["id"]
-
-        list_resp, list_body = list_api_keys(
+        try:
+            list_resp, list_body = list_api_keys(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                ocp_user_token=ocp_token_for_actor,
+                filters={"status": ["active"]},
+                sort={"by": "created_at", "order": "desc"},
+                pagination={"limit": 50, "offset": 0},
+            )
+            # existing assertions...
+        finally:
+            for key_id in (key1_id, key2_id):
+                revoke_api_key(
+                    request_session_http=request_session_http,
+                    base_url=base_url,
+                    key_id=key_id,
+                    ocp_user_token=ocp_token_for_actor,
+                )
-            request_session_http=request_session_http,
-            base_url=base_url,
-            ocp_user_token=ocp_token_for_actor,
-            filters={"status": ["active"]},
-            sort={"by": "created_at", "order": "desc"},
-            pagination={"limit": 50, "offset": 0},
-        )

As per coding guidelines, "**: REVIEW PRIORITIES: 3. Bug-prone patterns and error handling gaps".

Also applies to: 79-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 38 - 65, The test creates an API key with create_api_key and never
removes it, causing state leakage; wrap the create/get assertions in a
try/finally and in the finally call the existing deletion/revoke helper (e.g.,
delete_api_key or an equivalent API call using body["id"]) to ensure the key is
removed even if assertions fail, handling and logging any deletion errors (and
checking response codes) so cleanup won't silently fail; update both the block
around create_api_key/get_api_key and the similar block at the later section
(79-131) to use the same finally cleanup pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Around line 45-47: The test currently interpolates the full response body and
API key into assertion messages (variable body and body["key"]), which may leak
secrets on failure; update the assertions in the test (the checks referencing
body, "id", "key", and "name") to use generic, non-secret messages or masked
info only (e.g., assert "id" in body, assert "key" in body, assert "name" in
body) and replace any f-strings that include body or body["key"] with fixed
strings like "create response missing 'id'/'key'/'name'" or a masked summary (no
actual key material), ensuring no secrets from body are interpolated into
assertion messages or test logs.
- Around line 94-112: The test is flaky because it asserts list membership
immediately after creating keys; modify the block that calls list_api_keys so it
polls/retries until both key1_id and key2_id appear (or a timeout is reached)
instead of asserting immediately. Use list_api_keys as the poll function, check
items = list_body.get("items") or list_body.get("data") or [], and loop with a
short sleep/backoff and a max wait (e.g., retries or timeout) before failing; if
both key1_id and key2_id are present break and assert success, otherwise raise
the existing assertion after timeout.

In `@tests/model_serving/maas_billing/maas_subscription/utils.py`:
- Around line 222-232: Validate the key_id before building the URL in
get_api_key: ensure key_id is not empty or only whitespace and if invalid raise
a clear exception (e.g., ValueError) or return an error response immediately
instead of constructing f"{base_url}/v1/api-keys/{quote(key_id, safe='')}", to
avoid hitting the collection endpoint; apply the same validation to any other
helper that constructs /v1/api-keys/{id} (the similar block noted around lines
285-295) so all calls reject empty/whitespace IDs early with a consistent error.
- Line 238: The log lines write raw key_id which allows log-forging via control
characters; update the LOGGER.info calls that reference key_id (e.g., the
get_api_key logging statement and the similar one at the other occurrence) to
log a sanitized/escaped version instead of the raw value—produce an
escaped_key_id by encoding control characters (for example using
json.dumps(key_id) or replacing/remapping \n, \r, \t and other non-printable
chars to visible escape sequences) and use escaped_key_id in the log message so
no untrusted control characters can inject forged log lines.

---

Duplicate comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Around line 38-65: The test creates an API key with create_api_key and never
removes it, causing state leakage; wrap the create/get assertions in a
try/finally and in the finally call the existing deletion/revoke helper (e.g.,
delete_api_key or an equivalent API call using body["id"]) to ensure the key is
removed even if assertions fail, handling and logging any deletion errors (and
checking response codes) so cleanup won't silently fail; update both the block
around create_api_key/get_api_key and the similar block at the later section
(79-131) to use the same finally cleanup pattern.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: fc487078-7706-4e18-a100-35139a968860

📥 Commits

Reviewing files that changed from the base of the PR and between 000e0e5 and efc342d.

📒 Files selected for processing (2)
  • tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py
  • tests/model_serving/maas_billing/maas_subscription/utils.py

Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/utils.py
Comment thread tests/model_serving/maas_billing/maas_subscription/utils.py
Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py Outdated
SB159 and others added 4 commits March 17, 2026 18:00
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (4)

27-62: ⚠️ Potential issue | 🟠 Major

Created API key is not cleaned up (CWE-459: Incomplete Cleanup).

test_create_api_key creates a key but never revokes it. Unlike test_list_api_keys and test_revoke_api_key which now use fixtures, this test still has direct creation without teardown.

Proposed fix using try/finally
+from tests.model_serving.maas_billing.maas_subscription.utils import revoke_api_key
@@
     def test_create_api_key(
         self,
         request_session_http: requests.Session,
         base_url: str,
         ocp_token_for_actor: str,
     ) -> None:
         """Verify API key creation and show-once behavior."""

         key_name = f"e2e-crud-create-{generate_random_name()}"

         _, body = create_api_key(
             base_url=base_url,
             ocp_user_token=ocp_token_for_actor,
             request_session_http=request_session_http,
             api_key_name=key_name,
         )
+        key_id = body["id"]
+        try:
             for field in ("id", "key", "name"):
                 assert field in body, f"Expected '{field}' field in create response"

             key = body["key"]
             assert key.startswith("sk-oai-"), "Expected key to start with 'sk-oai-' prefix"
             assert len(key) > len("sk-oai-"), "Key body after prefix must not be empty"

-            LOGGER.info(f"[create] Created key id={body['id']}, key_prefix=sk-oai-***")
+            LOGGER.info(f"[create] Created key id={key_id}, key_prefix=sk-oai-***")

             get_resp, get_body = get_api_key(
                 request_session_http=request_session_http,
                 base_url=base_url,
-                key_id=body["id"],
+                key_id=key_id,
                 ocp_user_token=ocp_token_for_actor,
             )
             assert get_resp.status_code == 200, (
-                f"Expected 200 on GET /v1/api-keys/{body['id']}, got {get_resp.status_code}: {get_resp.text[:200]}"
+                f"Expected 200 on GET /v1/api-keys/{key_id}, got {get_resp.status_code}"
             )
             assert "key" not in get_body, "Plaintext key must not be returned by GET after creation (show-once pattern)"
+        finally:
+            revoke_api_key(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                key_id=key_id,
+                ocp_user_token=ocp_token_for_actor,
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 27 - 62, The test_create_api_key leaves a created API key active;
wrap the create/get assertions in a try/finally and call revoke_api_key in the
finally block to ensure cleanup; use the returned body["id"] from create_api_key
and the same ocp_token_for_actor and request_session_http when calling
revoke_api_key so the key is revoked regardless of assertion failures (refer to
test_create_api_key, create_api_key, get_api_key, revoke_api_key to locate the
relevant calls).

144-144: ⚠️ Potential issue | 🟠 Major

CWE-532: Full response body in assertion message may leak sensitive fields.

{get_body} dumps the entire response object on failure. Past review flagged this; still unaddressed. Log only the specific field being checked.

Proposed fix
-        assert get_body.get("status") == "revoked", f"Expected status='revoked' on GET after revoke, got: {get_body}"
+        assert get_body.get("status") == "revoked", f"Expected status='revoked', got status={get_body.get('status')!r}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py` at
line 144, The assertion exposes the full response body via f"{get_body}" which
may leak sensitive data; update the assertion in the test (around the check
using get_body and the assert currently comparing status == "revoked") to avoid
printing the whole response on failure—log only the specific field checked
(e.g., include get_body.get("status") or the status value) or a minimal
descriptive message referencing the "status" field instead of the entire
get_body object.

94-95: ⚠️ Potential issue | 🟠 Major

CWE-532: Full response object in assertion message may leak sensitive fields.

Dumping {item} on assertion failure exposes the entire dict to CI logs. Past review flagged this; still unaddressed.

Proposed fix
         for item in items:
-            assert "key" not in item, f"Plaintext key must not appear in any list item: {item}"
+            assert "key" not in item, "Plaintext key must not appear in any list item"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 94 - 95, The assertion currently prints the full item dict on
failure which may leak secrets; update the assertion in the loop over items so
it does not interpolate the entire dict into the message. Replace f"Plaintext
key must not appear in any list item: {item}" with a non-sensitive message
(e.g., "Plaintext key must not appear in any list item") or include only a safe
identifier (e.g., item.get("id") or the loop index) when available; target the
assertion around the items loop that checks "key" not in item.

75-92: ⚠️ Potential issue | 🟡 Minor

Eventual-consistency risk: no retry/polling before asserting list membership.

list_api_keys is called immediately after keys are created by the fixture. If the backend has indexing/replication delay, assertions at lines 88-92 become nondeterministic. Past review flagged this.

Proposed fix using polling
+from timeout_sampler import TimeoutSampler
@@
-        list_resp, list_body = list_api_keys(
-            request_session_http=request_session_http,
-            base_url=base_url,
-            ocp_user_token=ocp_token_for_actor,
-            filters={"status": ["active"]},
-            sort={"by": "created_at", "order": "desc"},
-            pagination={"limit": 50, "offset": 0},
-        )
-        assert list_resp.status_code == 200, (
-            f"Expected 200 on POST /v1/api-keys/search, got {list_resp.status_code}: {list_resp.text[:200]}"
-        )
-
-        items: list[dict] = list_body.get("items") or list_body.get("data") or []
-        assert len(items) >= 2, f"Expected at least 2 active keys, got {len(items)}"
-
-        key_ids = [item["id"] for item in items]
-        for created_id in two_active_api_key_ids:
-            assert created_id in key_ids, f"Created key id={created_id} not found in listed keys"
+        for sample in TimeoutSampler(
+            wait_timeout=60,
+            sleep=5,
+            func=list_api_keys,
+            request_session_http=request_session_http,
+            base_url=base_url,
+            ocp_user_token=ocp_token_for_actor,
+            filters={"status": ["active"]},
+            sort={"by": "created_at", "order": "desc"},
+            pagination={"limit": 50, "offset": 0},
+        ):
+            list_resp, list_body = sample
+            if list_resp.status_code != 200:
+                continue
+            items = list_body.get("items") or list_body.get("data") or []
+            key_ids = [item["id"] for item in items]
+            if all(cid in key_ids for cid in two_active_api_key_ids):
+                break
+        else:
+            pytest.fail(f"Created keys not visible in list within timeout")
+
+        assert len(items) >= 2, f"Expected at least 2 active keys, got {len(items)}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 75 - 92, The test currently asserts created API key IDs
(two_active_api_key_ids) are present immediately after calling list_api_keys,
which is flaky under eventual consistency; modify the test to poll/retry
list_api_keys until all expected ids appear or a timeout elapses: implement a
short loop (e.g., poll every 0.5–1s up to a configurable timeout like 10–30s)
that calls list_api_keys, extracts items (as the existing items: list[dict] =
... logic), checks that every id in two_active_api_key_ids is in the returned
key_ids, and only then proceeds to the existing assertions; if the timeout is
reached without seeing all ids, fail the test with a clear message. Use the
existing list_api_keys function and two_active_api_key_ids variable names so the
change is localized and easy to review.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_serving/maas_billing/maas_subscription/conftest.py`:
- Around line 561-580: The fixture two_active_api_key_ids creates API keys via
create_api_key but never revokes them, causing test-state leakage; change the
fixture to a yield-style fixture that returns the list of created IDs and then,
in the teardown after the yield, iterate over the IDs and call revoke_api_key
(using the same base_url, ocp_token_for_actor, and request_session_http) to
revoke each key, and log any revoke errors; ensure the fixture keeps the same
signature and scope and handles exceptions during cleanup so all created keys
are attempted to be revoked.
- Around line 583-600: The fixture active_api_key_id creates an API key but
never cleans it up, leaking test state; convert active_api_key_id into a
generator-style pytest fixture that yields the created key ID and then deletes
the key in teardown using the same HTTP session and tokens (use
request_session_http, base_url, ocp_token_for_actor) — mirror the pattern used
in two_active_api_key_ids: call create_api_key to produce body["id"], yield that
id, and after the yield call the API key deletion helper (e.g., delete_api_key
or the equivalent teardown call) to remove the created key and handle/log any
deletion errors.

---

Duplicate comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Around line 27-62: The test_create_api_key leaves a created API key active;
wrap the create/get assertions in a try/finally and call revoke_api_key in the
finally block to ensure cleanup; use the returned body["id"] from create_api_key
and the same ocp_token_for_actor and request_session_http when calling
revoke_api_key so the key is revoked regardless of assertion failures (refer to
test_create_api_key, create_api_key, get_api_key, revoke_api_key to locate the
relevant calls).
- Line 144: The assertion exposes the full response body via f"{get_body}" which
may leak sensitive data; update the assertion in the test (around the check
using get_body and the assert currently comparing status == "revoked") to avoid
printing the whole response on failure—log only the specific field checked
(e.g., include get_body.get("status") or the status value) or a minimal
descriptive message referencing the "status" field instead of the entire
get_body object.
- Around line 94-95: The assertion currently prints the full item dict on
failure which may leak secrets; update the assertion in the loop over items so
it does not interpolate the entire dict into the message. Replace f"Plaintext
key must not appear in any list item: {item}" with a non-sensitive message
(e.g., "Plaintext key must not appear in any list item") or include only a safe
identifier (e.g., item.get("id") or the loop index) when available; target the
assertion around the items loop that checks "key" not in item.
- Around line 75-92: The test currently asserts created API key IDs
(two_active_api_key_ids) are present immediately after calling list_api_keys,
which is flaky under eventual consistency; modify the test to poll/retry
list_api_keys until all expected ids appear or a timeout elapses: implement a
short loop (e.g., poll every 0.5–1s up to a configurable timeout like 10–30s)
that calls list_api_keys, extracts items (as the existing items: list[dict] =
... logic), checks that every id in two_active_api_key_ids is in the returned
key_ids, and only then proceeds to the existing assertions; if the timeout is
reached without seeing all ids, fail the test with a clear message. Use the
existing list_api_keys function and two_active_api_key_ids variable names so the
change is localized and easy to review.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 5cf48f20-e65a-485e-8244-8e4f9c89bd4e

📥 Commits

Reviewing files that changed from the base of the PR and between 8f48de7 and 72086c0.

📒 Files selected for processing (2)
  • tests/model_serving/maas_billing/maas_subscription/conftest.py
  • tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py

Comment thread tests/model_serving/maas_billing/maas_subscription/conftest.py Outdated
Comment thread tests/model_serving/maas_billing/maas_subscription/conftest.py Outdated
SB159 added 2 commits March 17, 2026 18:10
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
@SB159 SB159 force-pushed the feature/api-key-crud-tests branch from 72086c0 to 8b1bf1e Compare March 17, 2026 23:31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (4)
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (2)

27-62: ⚠️ Potential issue | 🟠 Major

test_create_api_key creates an API key without cleanup (test-state leakage).

This test creates an API key via create_api_key but does not use a fixture with teardown. The key persists after test completion, potentially causing quota issues or cross-test interference.

Proposed fix: wrap in try/finally with revoke_api_key
+from tests.model_serving.maas_billing.maas_subscription.utils import revoke_api_key
...
     def test_create_api_key(
         self,
         request_session_http: requests.Session,
         base_url: str,
         ocp_token_for_actor: str,
     ) -> None:
         """Verify API key creation and show-once behavior."""

         key_name = f"e2e-crud-create-{generate_random_name()}"

         _, body = create_api_key(
             base_url=base_url,
             ocp_user_token=ocp_token_for_actor,
             request_session_http=request_session_http,
             api_key_name=key_name,
         )
+        key_id = body["id"]
+        try:
             for field in ("id", "key", "name"):
                 assert field in body, f"Expected '{field}' field in create response"

             key = body["key"]
             assert key.startswith("sk-oai-"), "Expected key to start with 'sk-oai-' prefix"
             assert len(key) > len("sk-oai-"), "Key body after prefix must not be empty"

             LOGGER.info(f"[create] Created key id={body['id']}, key_prefix=sk-oai-***")

             get_resp, get_body = get_api_key(
                 request_session_http=request_session_http,
                 base_url=base_url,
-                key_id=body["id"],
+                key_id=key_id,
                 ocp_user_token=ocp_token_for_actor,
             )
             assert get_resp.status_code == 200, (
-                f"Expected 200 on GET /v1/api-keys/{body['id']}, got {get_resp.status_code}: {get_resp.text[:200]}"
+                f"Expected 200 on GET /v1/api-keys/{key_id}, got {get_resp.status_code}: {get_resp.text[:200]}"
             )
             assert "key" not in get_body, "Plaintext key must not be returned by GET after creation (show-once pattern)"
+        finally:
+            revoke_api_key(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                key_id=key_id,
+                ocp_user_token=ocp_token_for_actor,
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 27 - 62, test_create_api_key creates an API key via create_api_key
and never revokes it; wrap the key creation and assertions in a try/finally that
calls revoke_api_key(request_session_http=request_session_http,
base_url=base_url, key_id=body["id"], ocp_user_token=ocp_token_for_actor) in the
finally block so the created key is always removed even if assertions fail;
ensure body is set before the finally (e.g., declare body = None then assign)
and only call revoke_api_key when body and body["id"] exist.

94-95: ⚠️ Potential issue | 🟠 Major

CWE-532: Assertion message exposes full item dict which may contain sensitive fields.

On test failure, the assertion message dumps the entire item object to logs. Per past review, this was flagged but not addressed.

Proposed fix
         for item in items:
-            assert "key" not in item, f"Plaintext key must not appear in any list item: {item}"
+            assert "key" not in item, "Plaintext key must not appear in any list item"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 94 - 95, The assertion currently exposes the full sensitive item
dict in the failure message; update the assertion in the loop that iterates over
items (the check using "items" and the assertion line with f"Plaintext key must
not appear...") to remove the raw item dump—either use a generic message like
"Plaintext key must not appear in any list item" or include only a non-sensitive
identifier (e.g., item.get("id") or a masked value) instead of the full item
dict so the test failure does not log sensitive fields.
tests/model_serving/maas_billing/maas_subscription/conftest.py (2)

674-680: ⚠️ Potential issue | 🟡 Minor

Teardown lacks exception handling.

Same pattern issue: revoke_api_key can raise AssertionError. Wrap in try/except to log and suppress cleanup failures.

Proposed fix
     yield body["id"]
-    LOGGER.info(f"Fixture teardown: revoking key {body['id']}")
-    revoke_api_key(
-        request_session_http=request_session_http,
-        base_url=base_url,
-        key_id=body["id"],
-        ocp_user_token=ocp_token_for_actor,
-    )
+    try:
+        LOGGER.info(f"Fixture teardown: revoking key {body['id']}")
+        revoke_api_key(
+            request_session_http=request_session_http,
+            base_url=base_url,
+            key_id=body["id"],
+            ocp_user_token=ocp_token_for_actor,
+        )
+    except Exception as exc:
+        LOGGER.warning(f"Cleanup failed for key {body['id']}: {exc}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/conftest.py` around lines
674 - 680, The fixture teardown calls revoke_api_key(...) which can raise
AssertionError but is not handled; wrap the revoke_api_key invocation in a
try/except that catches AssertionError (optionally Exception for safety), log
the failure via LOGGER.error including the key id (body['id']) and the exception
details, and suppress the exception so teardown continues; keep the existing
LOGGER.info call and pass the same parameters (request_session_http, base_url,
key_id=body["id"], ocp_user_token=ocp_token_for_actor) inside the try block.

646-653: ⚠️ Potential issue | 🟡 Minor

Teardown loop lacks exception handling; failure on first key leaks subsequent keys.

If revoke_api_key raises (e.g., AssertionError on non-JSON response per utils.py:303-306), the loop terminates and remaining keys are never revoked.

Proposed fix
     yield ids
     for key_id in ids:
-        LOGGER.info(f"Fixture teardown: revoking key {key_id}")
-        revoke_api_key(
-            request_session_http=request_session_http,
-            base_url=base_url,
-            key_id=key_id,
-            ocp_user_token=ocp_token_for_actor,
-        )
+        try:
+            LOGGER.info(f"Fixture teardown: revoking key {key_id}")
+            revoke_api_key(
+                request_session_http=request_session_http,
+                base_url=base_url,
+                key_id=key_id,
+                ocp_user_token=ocp_token_for_actor,
+            )
+        except Exception as exc:
+            LOGGER.warning(f"Cleanup failed for key {key_id}: {exc}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/conftest.py` around lines
646 - 653, The teardown loop over ids should not abort on the first revoke
failure; wrap the call to revoke_api_key inside a try/except that catches
Exception, logs an error with the key_id and exception (using LOGGER) and
continues to the next id so all keys are attempted; ensure you still pass
request_session_http, base_url and ocp_token_for_actor to revoke_api_key and
consider logging a final summary if any revokes failed.
🧹 Nitpick comments (1)
tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py (1)

99-122: Test passes vacuously if no active keys exist.

assert len(paged_items) <= 1 succeeds when paged_items is empty. This doesn't validate pagination logic—it only confirms the API didn't return more than requested. Consider asserting len(paged_items) == 1 after ensuring at least one key exists via fixture.

Proposed fix: use fixture to guarantee keys exist
     `@pytest.mark.tier1`
     `@pytest.mark.parametrize`("ocp_token_for_actor", [{"type": "admin"}], indirect=True)
     def test_list_api_keys_pagination(
         self,
         request_session_http: requests.Session,
         base_url: str,
         ocp_token_for_actor: str,
+        two_active_api_key_ids: list[str],  # ensures at least 2 keys exist
     ) -> None:
         """Verify that the search endpoint respects the pagination limit."""
         ...
         paged_items: list[dict] = page_body.get("items") or page_body.get("data") or []
-        assert len(paged_items) <= 1, f"Expected at most 1 item with limit=1, got {len(paged_items)}"
+        assert len(paged_items) == 1, f"Expected exactly 1 item with limit=1, got {len(paged_items)}"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`
around lines 99 - 122, The test test_list_api_keys_pagination can pass vacuously
when no active API keys exist because it only asserts len(paged_items) <= 1;
update the test to ensure at least one active key exists (use or add a
fixture/helper that creates an active key via the same API used in
list_api_keys) and then change the assertion to assert len(paged_items) == 1 to
validate pagination behavior; reference list_api_keys and the paged_items
variable when adding the setup fixture or call so the test reliably exercises
the pagination logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/model_serving/maas_billing/maas_subscription/conftest.py`:
- Around line 683-708: The teardown for active_api_key_id must tolerate
double-revocation to avoid teardown failures after revoked_api_key_id already
revoked it: update the active_api_key_id fixture's finalizer to call
revoke_api_key inside a try/except and treat expected outcomes (HTTP 200 with
status='revoked', HTTP 409 conflict, or a non-JSON/non-200 response indicating
"already revoked") as non-fatal; specifically, catch JSON decode errors and
request exceptions, log them at debug/info, and only raise/assert on unexpected
error codes or responses. Use the existing revoke_api_key helper and reference
the fixtures active_api_key_id and revoked_api_key_id so the teardown becomes
idempotent and safe against double revocation.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Line 144: The assertion exposes the full response body via {get_body}; change
the message to avoid dumping sensitive data by only including non-sensitive
fields (e.g., the status) or a minimal identifier: update the assertion in
test_api_key_crud.py that reads assert get_body.get("status") == "revoked",
f"Expected status='revoked' on GET after revoke, got: {get_body}" to something
like asserting the same condition but using a message that references only
get_body.get("status") (for example: f"Expected status='revoked' on GET after
revoke, got status={get_body.get('status')}") so the test still reports useful
info without printing the entire response body.

---

Duplicate comments:
In `@tests/model_serving/maas_billing/maas_subscription/conftest.py`:
- Around line 674-680: The fixture teardown calls revoke_api_key(...) which can
raise AssertionError but is not handled; wrap the revoke_api_key invocation in a
try/except that catches AssertionError (optionally Exception for safety), log
the failure via LOGGER.error including the key id (body['id']) and the exception
details, and suppress the exception so teardown continues; keep the existing
LOGGER.info call and pass the same parameters (request_session_http, base_url,
key_id=body["id"], ocp_user_token=ocp_token_for_actor) inside the try block.
- Around line 646-653: The teardown loop over ids should not abort on the first
revoke failure; wrap the call to revoke_api_key inside a try/except that catches
Exception, logs an error with the key_id and exception (using LOGGER) and
continues to the next id so all keys are attempted; ensure you still pass
request_session_http, base_url and ocp_token_for_actor to revoke_api_key and
consider logging a final summary if any revokes failed.

In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Around line 27-62: test_create_api_key creates an API key via create_api_key
and never revokes it; wrap the key creation and assertions in a try/finally that
calls revoke_api_key(request_session_http=request_session_http,
base_url=base_url, key_id=body["id"], ocp_user_token=ocp_token_for_actor) in the
finally block so the created key is always removed even if assertions fail;
ensure body is set before the finally (e.g., declare body = None then assign)
and only call revoke_api_key when body and body["id"] exist.
- Around line 94-95: The assertion currently exposes the full sensitive item
dict in the failure message; update the assertion in the loop that iterates over
items (the check using "items" and the assertion line with f"Plaintext key must
not appear...") to remove the raw item dump—either use a generic message like
"Plaintext key must not appear in any list item" or include only a non-sensitive
identifier (e.g., item.get("id") or a masked value) instead of the full item
dict so the test failure does not log sensitive fields.

---

Nitpick comments:
In `@tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py`:
- Around line 99-122: The test test_list_api_keys_pagination can pass vacuously
when no active API keys exist because it only asserts len(paged_items) <= 1;
update the test to ensure at least one active key exists (use or add a
fixture/helper that creates an active key via the same API used in
list_api_keys) and then change the assertion to assert len(paged_items) == 1 to
validate pagination behavior; reference list_api_keys and the paged_items
variable when adding the setup fixture or call so the test reliably exercises
the pagination logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 66f5f173-4c30-48a5-b33a-a54368d2ccfc

📥 Commits

Reviewing files that changed from the base of the PR and between 72086c0 and 8b1bf1e.

📒 Files selected for processing (3)
  • tests/model_serving/maas_billing/maas_subscription/conftest.py
  • tests/model_serving/maas_billing/maas_subscription/test_api_key_crud.py
  • tests/model_serving/maas_billing/maas_subscription/utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/model_serving/maas_billing/maas_subscription/utils.py

Comment thread tests/model_serving/maas_billing/maas_subscription/conftest.py
@SB159 SB159 requested a review from dbasunag March 17, 2026 23:39
Copy link
Copy Markdown
Contributor

@sheltoncyril sheltoncyril left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@sheltoncyril sheltoncyril merged commit f26ce5e into opendatahub-io:main Mar 18, 2026
16 checks passed
@github-actions
Copy link
Copy Markdown

Status of building tag latest: success.
Status of pushing tag latest to image registry: success.

ssaleem-rh pushed a commit to ssaleem-rh/opendatahub-tests that referenced this pull request Mar 23, 2026
* test: add MaaS API key tests

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* address coderabit review comments

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>

* address coderabit comments

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>

* address review comment

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>

* address latest coderabit review comments

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>

---------

Signed-off-by: Swati Mukund Bagal <sbagal@redhat.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Signed-off-by: Shehan Saleem <ssaleem@redhat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants