"""Tests for proxy-level API key authentication.""" import pytest from fastapi.testclient import TestClient from headroom.proxy.models import ProxyConfig from headroom.proxy.server import _proxy_config_from_env, create_app @pytest.fixture() def api_client(): """Create a TestClient with API key auth enabled.""" config = ProxyConfig( proxy_api_key="test-secret-key", require_proxy_api_key=True, cache_enabled=False, rate_limit_enabled=False, log_requests=False, ) with TestClient(create_app(config)) as client: yield client @pytest.fixture() def open_client(): """Create a TestClient with API key auth disabled.""" config = ProxyConfig( cache_enabled=False, rate_limit_enabled=False, log_requests=False, ) with TestClient(create_app(config)) as client: yield client class TestHealthEndpointsAlwaysAccessible: """Health endpoints must remain accessible without API key.""" def test_livez_no_key(self, api_client): resp = api_client.get("/livez") assert resp.status_code == 200 def test_healthz_no_key(self, api_client): resp = api_client.get("/healthz") assert resp.status_code == 200 def test_readyz_no_key(self, api_client): resp = api_client.get("/readyz") assert resp.status_code == 200 def test_health_no_key(self, api_client): resp = api_client.get("/health") assert resp.status_code == 200 def test_visualize_no_key(self, api_client): resp = api_client.get("/visualize") assert resp.status_code == 200 def test_dashboard_no_key(self, api_client): resp = api_client.get("/dashboard") assert resp.status_code == 200 class TestProtectedEndpointsRejectWithoutKey: """Functional endpoints must reject requests without a valid key.""" def test_stats_no_key(self, api_client): resp = api_client.get("/stats") assert resp.status_code == 401 def test_stats_wrong_key(self, api_client): resp = api_client.get("/stats", headers={"Authorization": "Bearer wrong"}) assert resp.status_code == 401 def test_cache_clear_no_key(self, api_client): resp = api_client.post("/cache/clear") assert resp.status_code == 401 def test_quota_no_key(self, api_client): resp = api_client.get("/quota") assert resp.status_code == 401 class TestProtectedEndpointsAcceptValidKey: """Functional endpoints must accept requests with the correct key.""" def test_stats_with_key(self, api_client): resp = api_client.get("/stats", headers={"Authorization": "Bearer test-secret-key"}) assert resp.status_code == 200 def test_quota_with_key(self, api_client): resp = api_client.get("/quota", headers={"Authorization": "Bearer test-secret-key"}) assert resp.status_code == 200 def test_cache_clear_with_key(self, api_client): resp = api_client.post("/cache/clear", headers={"Authorization": "Bearer test-secret-key"}) assert resp.status_code == 200 class TestAuthDisabledByDefault: """When no API key is configured, all endpoints are open.""" def test_stats_open(self, open_client): resp = open_client.get("/stats") assert resp.status_code == 200 def test_cache_clear_open(self, open_client): resp = open_client.post("/cache/clear") assert resp.status_code == 200 class TestEnvConfig: """The proxy should pick up API key auth from the environment.""" def test_env_enables_proxy_key(self, monkeypatch): monkeypatch.setenv("HEADROOM_PROXY_API_KEY", "env-secret-key") config = _proxy_config_from_env() assert config.proxy_api_key == "env-secret-key" assert config.require_proxy_api_key is True class TestStartupValidation: """Proxy must fail fast if misconfigured.""" def test_raises_when_key_missing(self): config = ProxyConfig(require_proxy_api_key=True, proxy_api_key=None) with pytest.raises(ValueError, match="proxy_api_key is required"): create_app(config)