"""Server Middleware 单元测试 - API Key Auth + Rate Limiting""" import os import time import pytest from unittest.mock import patch from fastapi import FastAPI from fastapi.testclient import TestClient from agentkit.server.middleware import ( APIKeyAuthMiddleware, RateLimiter, RateLimitMiddleware, ) # --------------------------------------------------------------------------- # Helper: minimal app with only a health endpoint for isolated middleware tests # --------------------------------------------------------------------------- def _make_minimal_app(): """Create a minimal FastAPI app with just a health endpoint.""" app = FastAPI() @app.get("/api/v1/health") async def health(): return {"status": "ok"} @app.get("/api/v1/protected") async def protected(): return {"data": "secret"} return app # --------------------------------------------------------------------------- # APIKeyAuthMiddleware Tests # --------------------------------------------------------------------------- class TestAPIKeyAuthMiddleware: """API Key authentication middleware tests.""" def test_dev_mode_no_api_key_set_passes_through(self): """No api_key passed → requests pass through (dev mode).""" with patch.dict(os.environ, {}, clear=False): # Ensure AGENTKIT_API_KEY is not set os.environ.pop("AGENTKIT_API_KEY", None) app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware) client = TestClient(app) response = client.get("/api/v1/protected") assert response.status_code == 200 def test_api_key_set_no_header_returns_401(self): """api_key passed, no header → 401.""" app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware, api_key="test-secret-key") client = TestClient(app) response = client.get("/api/v1/protected") assert response.status_code == 401 data = response.json() assert data["error"] == "Unauthorized" def test_api_key_set_wrong_header_returns_401(self): """api_key passed, wrong header → 401.""" app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware, api_key="test-secret-key") client = TestClient(app) response = client.get( "/api/v1/protected", headers={"X-API-Key": "wrong-key"}, ) assert response.status_code == 401 def test_api_key_set_correct_header_returns_200(self): """api_key passed, correct header → 200.""" app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware, api_key="test-secret-key") client = TestClient(app) response = client.get( "/api/v1/protected", headers={"X-API-Key": "test-secret-key"}, ) assert response.status_code == 200 assert response.json()["data"] == "secret" def test_health_check_path_no_auth_required(self): """Health check path → 200 without API key.""" app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware, api_key="test-secret-key") client = TestClient(app) response = client.get("/api/v1/health") assert response.status_code == 200 assert response.json()["status"] == "ok" def test_programmatic_api_key_parameter(self): """Programmatic api_key parameter → uses passed key.""" with patch.dict(os.environ, {}, clear=False): os.environ.pop("AGENTKIT_API_KEY", None) app = _make_minimal_app() app.add_middleware(APIKeyAuthMiddleware, api_key="programmatic-key") client = TestClient(app) response = client.get( "/api/v1/protected", headers={"X-API-Key": "programmatic-key"}, ) assert response.status_code == 200 # --------------------------------------------------------------------------- # RateLimiter Tests # --------------------------------------------------------------------------- class TestRateLimiter: """Fixed-window rate limiter unit tests.""" def test_requests_within_limit_allowed(self): """Requests within limit → allowed.""" limiter = RateLimiter(max_requests=5, window_seconds=60) for i in range(5): allowed, retry_after = limiter.is_allowed("test-key") assert allowed is True assert retry_after == 0.0 def test_requests_exceed_limit_denied(self): """Requests exceed limit → denied with retry_after.""" limiter = RateLimiter(max_requests=2, window_seconds=60) # Use up the limit limiter.is_allowed("test-key") limiter.is_allowed("test-key") # Next request should be denied allowed, retry_after = limiter.is_allowed("test-key") assert allowed is False assert retry_after > 0 def test_after_window_expires_counter_resets(self): """After window expires → counter resets.""" limiter = RateLimiter(max_requests=2, window_seconds=1) # Use up the limit limiter.is_allowed("test-key") limiter.is_allowed("test-key") # Should be denied allowed, _ = limiter.is_allowed("test-key") assert allowed is False # Wait for window to expire time.sleep(1.1) # Should be allowed again allowed, retry_after = limiter.is_allowed("test-key") assert allowed is True def test_different_keys_independent_counters(self): """Different keys have independent counters.""" limiter = RateLimiter(max_requests=1, window_seconds=60) # Use up key-a's limit limiter.is_allowed("key-a") # key-a should be denied allowed_a, _ = limiter.is_allowed("key-a") assert allowed_a is False # key-b should still be allowed allowed_b, _ = limiter.is_allowed("key-b") assert allowed_b is True def test_max_requests_property(self): """max_requests property returns configured value.""" limiter = RateLimiter(max_requests=100, window_seconds=30) assert limiter.max_requests == 100 # --------------------------------------------------------------------------- # RateLimitMiddleware Tests # --------------------------------------------------------------------------- class TestRateLimitMiddleware: """Rate limiting middleware integration tests.""" def test_returns_429_with_retry_after_header(self): """Returns 429 with Retry-After header when limit exceeded.""" app = _make_minimal_app() app.add_middleware(RateLimitMiddleware, max_requests=1, window_seconds=60) client = TestClient(app) # First request should pass response1 = client.get("/api/v1/protected") assert response1.status_code == 200 # Second request should be rate limited response2 = client.get("/api/v1/protected") assert response2.status_code == 429 data = response2.json() assert data["error"] == "Too Many Requests" assert "Retry-After" in response2.headers def test_uses_api_key_for_identity(self): """Uses API key for identity when present (different keys = different limits).""" app = _make_minimal_app() app.add_middleware(RateLimitMiddleware, max_requests=1, window_seconds=60) client = TestClient(app) # Request with key-a response_a1 = client.get( "/api/v1/protected", headers={"X-API-Key": "key-a"}, ) assert response_a1.status_code == 200 # key-a should now be rate limited response_a2 = client.get( "/api/v1/protected", headers={"X-API-Key": "key-a"}, ) assert response_a2.status_code == 429 # key-b should still be allowed (independent counter) response_b1 = client.get( "/api/v1/protected", headers={"X-API-Key": "key-b"}, ) assert response_b1.status_code == 200