"""Tests for rate limiting on auth endpoints.""" import pytest from unittest.mock import Mock, patch, MagicMock from fastapi.testclient import TestClient from SPARC.api import app @pytest.fixture def client(): """Create test client with rate limiter enabled.""" return TestClient(app) @pytest.fixture(autouse=True) def reset_limiter(): """Reset rate limiter storage between tests.""" from SPARC.api import limiter limiter.reset() yield class TestRateLimiting: """Test rate limiting on login and register endpoints.""" @patch("SPARC.api.get_db_client") def test_login_allows_requests_under_limit(self, mock_db_client, client): """Login endpoint allows requests under the rate limit.""" mock_db = MagicMock() mock_db.authenticate_user.return_value = None mock_db_client.return_value = mock_db # Should allow at least a few requests for _ in range(5): response = client.post( "/auth/login", json={"email": "test@example.com", "password": "password123"}, ) # 401 is expected (invalid credentials), not 429 assert response.status_code == 401 @patch("SPARC.api.get_db_client") def test_login_rate_limited_after_threshold(self, mock_db_client, client): """Login endpoint returns 429 after exceeding rate limit.""" mock_db = MagicMock() mock_db.authenticate_user.return_value = None mock_db_client.return_value = mock_db # Send more than the limit (10/minute) statuses = [] for _ in range(15): response = client.post( "/auth/login", json={"email": "test@example.com", "password": "password123"}, ) statuses.append(response.status_code) # At least one should be 429 assert 429 in statuses, f"Expected 429 in statuses but got: {set(statuses)}" @patch("SPARC.api.get_db_client") def test_register_rate_limited_after_threshold(self, mock_db_client, client): """Register endpoint returns 429 after exceeding rate limit.""" mock_db = MagicMock() mock_db.get_user_count.return_value = 1 mock_db.create_user.return_value = None # triggers 400 (email exists) mock_db_client.return_value = mock_db # Send more than the limit (5/minute) statuses = [] for _ in range(10): response = client.post( "/auth/register", json={"email": "test@example.com", "password": "password123"}, ) statuses.append(response.status_code) # At least one should be 429 assert 429 in statuses, f"Expected 429 in statuses but got: {set(statuses)}" @patch("SPARC.api.get_db_client") def test_rate_limit_returns_retry_after_header(self, mock_db_client, client): """Rate limited responses include a Retry-After header.""" mock_db = MagicMock() mock_db.authenticate_user.return_value = None mock_db_client.return_value = mock_db # Exhaust the limit for _ in range(15): response = client.post( "/auth/login", json={"email": "test@example.com", "password": "password123"}, ) if response.status_code == 429: assert "Retry-After" in response.headers break