forked from 0xWheatyz/SPARC
b32eebff8a
Uncomment the ruff check and pytest steps in the Gitea Actions build workflow so that linting violations and test failures block image builds. Fix all pre-existing ruff violations (E402 import ordering in analyzer.py, F821 undefined name in api.py, I001 unsorted imports in test files, F401 unused import in test_rate_limit.py). Closes leeworks-agents/SPARC#1559 Closes leeworks-agents/SPARC#1560 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""Tests for rate limiting on auth endpoints."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
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
|