"""Admin API tests (read-only). Two layers: - always: with the project key, every /v1/organization/* GET must be rejected 401/403 with a "Missing scopes" message (documents the ACCOUNT_RESTRICTED state observed on 2026-09-18) — unless OPENAI_ADMIN_KEY is set, then skipped; - RUN_ADMIN_TESTS=true + OPENAI_ADMIN_KEY: real list calls succeed (never mutating). Webhook endpoints list / event types work with a project key and are asserted here too (LIVE_VERIFIED 2026-09-18). """ from __future__ import annotations import os import time import pytest ADMIN_GETS = [ ("/v1/organization/users?limit=1", "api.management.read"), ("/v1/organization/invites?limit=1", "api.management.read"), ("/v1/organization/projects?limit=1", "api.management.read"), ("/v1/organization/admin_api_keys?limit=1", "api.management.read"), ("/v1/organization/audit_logs?limit=1", "api.audit_logs.read"), ("/v1/organization/roles?limit=1", "api.roles.read"), ("/v1/organization/groups?limit=1", "api.groups.read"), ("/v1/organization/certificates?limit=1", "api.mtls.read"), ("/v1/organization/external_storage", "api.external_storage.read"), ("/v1/organization/spend_limit", "api.management.read"), ("/v1/organization/data_retention", "api.management.read"), ] @pytest.mark.parametrize("path,scope", ADMIN_GETS) def test_admin_get_requires_scope_with_project_key(openai, path, scope): if os.environ.get("OPENAI_ADMIN_KEY"): pytest.skip("Admin key configured: restriction not reproducible with the project key") st, body, _ = openai("GET", path, note="test_admin restricted probe") assert st in (401, 403), (st, body) err = body["error"] msg = err if isinstance(err, str) else err["message"] assert "Missing scopes" in msg and scope in msg def test_usage_and_costs_require_usage_scope(openai): if os.environ.get("OPENAI_ADMIN_KEY"): pytest.skip("Admin key configured") start = int(time.time()) - 86400 for path in (f"/v1/organization/usage/completions?start_time={start}&limit=1", f"/v1/organization/costs?start_time={start}&limit=1"): st, body, _ = openai("GET", path, note="test_admin usage probe") assert st == 403 and "api.usage.read" in str(body["error"]) def test_webhook_endpoints_list_works_with_project_key(openai): st, body, hdrs = openai("GET", "/v1/webhook_endpoints?limit=1", note="test_admin webhook endpoints list") assert st == 200 and body["object"] == "list" and {"data", "first_id", "last_id", "has_more"} <= set(body) def test_webhook_event_types_list(openai): st, body, _ = openai("GET", "/v1/webhook_event_types", note="test_admin webhook event types") assert st == 200 and body["object"] == "list" types = set(body["data"]) assert {"response.completed", "batch.completed", "fine_tuning.job.succeeded", "eval.run.succeeded", "realtime.call.incoming"} <= types # ------------------------------------------------------------------ gated real admin tests @pytest.mark.run_admin_tests def test_admin_list_users_projects(openai): if not os.environ.get("OPENAI_ADMIN_KEY"): pytest.skip("OPENAI_ADMIN_KEY not set") for path in ("/v1/organization/users?limit=5", "/v1/organization/projects?limit=5", "/v1/organization/audit_logs?limit=5"): st, body, _ = openai("GET", path, admin=True, note="test_admin real list") assert st == 200 and body["object"] == "list", (path, st, body) @pytest.mark.run_admin_tests def test_admin_usage_costs(openai): if not os.environ.get("OPENAI_ADMIN_KEY"): pytest.skip("OPENAI_ADMIN_KEY not set") start = int(time.time()) - 7 * 86400 st, body, _ = openai("GET", f"/v1/organization/usage/completions?start_time={start}&bucket_width=1d&group_by=model&limit=7", admin=True, note="test_admin usage") assert st == 200 and body["object"] == "page" and "data" in body st, body, _ = openai("GET", f"/v1/organization/costs?start_time={start}&bucket_width=1d&limit=7", admin=True, note="test_admin costs") assert st == 200 and body["object"] == "page"