11"""Tests for API key authentication."""
22
33import uuid
4+ from typing import Any
45
56import pytest
7+ from fastapi import Request
68from fastapi .testclient import TestClient
79
810from agent_control_server import __version__ as server_version
11+ from agent_control_server .auth_framework import Operation , Principal , set_authorizer
912from agent_control_server .config import auth_settings
1013
1114from .utils import VALID_CONTROL_PAYLOAD
1215
1316
17+ class _RecordingAuthorizer :
18+ """Test authorizer that records the operation requested by a route."""
19+
20+ def __init__ (self ) -> None :
21+ self .calls : list [tuple [Operation , dict [str , Any ] | None ]] = []
22+
23+ async def authorize (
24+ self ,
25+ request : Request ,
26+ operation : Operation ,
27+ context : dict [str , Any ] | None = None ,
28+ ) -> Principal :
29+ del request
30+ self .calls .append ((operation , context ))
31+ return Principal (namespace_key = "default" )
32+
33+
1434class TestHealthEndpoint :
1535 """Health endpoint should always be accessible without authentication."""
1636
@@ -40,9 +60,7 @@ class TestProtectedEndpoints:
4060 def test_missing_api_key_returns_401 (self , unauthenticated_client : TestClient ) -> None :
4161 """Given no API key, when requesting protected endpoint, then returns 401."""
4262 # When:
43- response = unauthenticated_client .get (
44- "/api/v1/agents/00000000-0000-0000-0000-000000000000"
45- )
63+ response = unauthenticated_client .get ("/api/v1/agents/00000000-0000-0000-0000-000000000000" )
4664
4765 # Then:
4866 assert response .status_code == 401
@@ -111,6 +129,20 @@ def test_missing_key_returns_401_on_evaluators(
111129 # Then:
112130 assert response .status_code == 401
113131
132+ def test_evaluators_use_auth_framework_provider (self , app : object ) -> None :
133+ """Given a custom authorizer, when listing evaluators, then route uses it."""
134+ # Given:
135+ authorizer = _RecordingAuthorizer ()
136+ set_authorizer (authorizer )
137+ client = TestClient (app , raise_server_exceptions = True )
138+
139+ # When:
140+ response = client .get ("/api/v1/evaluators" )
141+
142+ # Then:
143+ assert response .status_code == 200
144+ assert authorizer .calls == [(Operation .EVALUATORS_READ , None )]
145+
114146
115147class TestAuthDisabled :
116148 """When auth is disabled, all requests should succeed."""
@@ -120,21 +152,15 @@ def disable_auth(self, monkeypatch: pytest.MonkeyPatch) -> None:
120152 """Disable auth for tests in this class."""
121153 monkeypatch .setattr (auth_settings , "api_key_enabled" , False )
122154
123- def test_no_key_allowed_when_disabled (
124- self , unauthenticated_client : TestClient
125- ) -> None :
155+ def test_no_key_allowed_when_disabled (self , unauthenticated_client : TestClient ) -> None :
126156 """Given auth disabled, when requesting without API key, then request succeeds."""
127157 # When:
128- response = unauthenticated_client .get (
129- "/api/v1/agents/00000000-0000-0000-0000-000000000000"
130- )
158+ response = unauthenticated_client .get ("/api/v1/agents/00000000-0000-0000-0000-000000000000" )
131159
132160 # Then: (404 for non-existent resource, but NOT 401)
133161 assert response .status_code == 404
134162
135- def test_evaluators_accessible_when_disabled (
136- self , unauthenticated_client : TestClient
137- ) -> None :
163+ def test_evaluators_accessible_when_disabled (self , unauthenticated_client : TestClient ) -> None :
138164 """Given auth disabled, when listing evaluators without API key, then returns 200."""
139165 # When:
140166 response = unauthenticated_client .get ("/api/v1/evaluators" )
@@ -264,9 +290,7 @@ def test_admin_key_allowed_on_representative_mutations(self, admin_client: TestC
264290 init_response = admin_client .post ("/api/v1/agents/initAgent" , json = init_payload )
265291 assert init_response .status_code == 200
266292
267- set_policy_response = admin_client .post (
268- f"/api/v1/agents/{ agent_name } /policy/{ policy_id } "
269- )
293+ set_policy_response = admin_client .post (f"/api/v1/agents/{ agent_name } /policy/{ policy_id } " )
270294 assert set_policy_response .status_code == 200
271295
272296
@@ -344,9 +368,7 @@ def setup_no_keys(self, monkeypatch: pytest.MonkeyPatch) -> None:
344368 def test_misconfigured_returns_500 (self , unauthenticated_client : TestClient ) -> None :
345369 """Given auth enabled but no keys configured, when requesting, then returns 500."""
346370 # When:
347- response = unauthenticated_client .get (
348- "/api/v1/agents/00000000-0000-0000-0000-000000000000"
349- )
371+ response = unauthenticated_client .get ("/api/v1/agents/00000000-0000-0000-0000-000000000000" )
350372
351373 # Then:
352374 assert response .status_code == 500
@@ -360,6 +382,7 @@ class TestOptionalApiKey:
360382
361383 def _make_optional_app (self ) -> TestClient :
362384 from fastapi import Depends , FastAPI
385+
363386 from agent_control_server .auth import optional_api_key
364387
365388 app = FastAPI ()
@@ -374,7 +397,9 @@ def maybe_auth(client=Depends(optional_api_key)) -> dict[str, object]:
374397
375398 return TestClient (app )
376399
377- def test_optional_api_key_auth_disabled_returns_none (self , monkeypatch : pytest .MonkeyPatch ) -> None :
400+ def test_optional_api_key_auth_disabled_returns_none (
401+ self , monkeypatch : pytest .MonkeyPatch
402+ ) -> None :
378403 # Given: auth disabled
379404 monkeypatch .setattr (auth_settings , "api_key_enabled" , False )
380405
@@ -386,7 +411,9 @@ def test_optional_api_key_auth_disabled_returns_none(self, monkeypatch: pytest.M
386411 assert response .status_code == 200
387412 assert response .json ()["auth" ] is False
388413
389- def test_optional_api_key_missing_header_returns_none (self , monkeypatch : pytest .MonkeyPatch ) -> None :
414+ def test_optional_api_key_missing_header_returns_none (
415+ self , monkeypatch : pytest .MonkeyPatch
416+ ) -> None :
390417 # Given: auth enabled with configured keys
391418 monkeypatch .setattr (auth_settings , "api_key_enabled" , True )
392419 monkeypatch .setattr (auth_settings , "api_keys" , "user-key" )
@@ -402,7 +429,9 @@ def test_optional_api_key_missing_header_returns_none(self, monkeypatch: pytest.
402429 assert response .status_code == 200
403430 assert response .json ()["auth" ] is False
404431
405- def test_optional_api_key_invalid_header_returns_none (self , monkeypatch : pytest .MonkeyPatch ) -> None :
432+ def test_optional_api_key_invalid_header_returns_none (
433+ self , monkeypatch : pytest .MonkeyPatch
434+ ) -> None :
406435 # Given: auth enabled with configured keys
407436 monkeypatch .setattr (auth_settings , "api_key_enabled" , True )
408437 monkeypatch .setattr (auth_settings , "api_keys" , "user-key" )
@@ -418,7 +447,9 @@ def test_optional_api_key_invalid_header_returns_none(self, monkeypatch: pytest.
418447 assert response .status_code == 200
419448 assert response .json ()["auth" ] is False
420449
421- def test_optional_api_key_admin_header_sets_admin (self , monkeypatch : pytest .MonkeyPatch ) -> None :
450+ def test_optional_api_key_admin_header_sets_admin (
451+ self , monkeypatch : pytest .MonkeyPatch
452+ ) -> None :
422453 # Given: auth enabled with admin key
423454 monkeypatch .setattr (auth_settings , "api_key_enabled" , True )
424455 monkeypatch .setattr (auth_settings , "api_keys" , "user-key" )
@@ -449,6 +480,7 @@ def test_require_admin_key_rejects_non_admin(
449480
450481 # When: requiring admin key on an endpoint
451482 from fastapi import Depends , FastAPI
483+
452484 from agent_control_server .auth import require_admin_key
453485
454486 local_app = FastAPI ()
@@ -483,6 +515,7 @@ def test_authenticated_client_key_id_masks_short_key(self) -> None:
483515 def test_get_api_key_from_header_extracts_value (self ) -> None :
484516 # Given: a route that returns raw API key header
485517 from fastapi import Depends , FastAPI
518+
486519 from agent_control_server .auth import get_api_key_from_header
487520
488521 app = FastAPI ()
@@ -503,6 +536,7 @@ def raw_key(key: str | None = Depends(get_api_key_from_header)) -> dict[str, str
503536 def test_get_api_key_from_header_allows_missing (self ) -> None :
504537 # Given: a route that returns raw API key header
505538 from fastapi import Depends , FastAPI
539+
506540 from agent_control_server .auth import get_api_key_from_header
507541
508542 app = FastAPI ()
0 commit comments