Files
PROJECT-CONTORL/backend/tests/test_rate_limit.py
beabigegg 9b220523ff feat: complete issue fixes and implement remaining features
## Critical Issues (CRIT-001~003) - All Fixed
- JWT secret key validation with pydantic field_validator
- Login audit logging for success/failure attempts
- Frontend API path prefix removal

## High Priority Issues (HIGH-001~008) - All Fixed
- Project soft delete using is_active flag
- Redis session token bytes handling
- Rate limiting with slowapi (5 req/min for login)
- Attachment API permission checks
- Kanban view with drag-and-drop
- Workload heatmap UI (WorkloadPage, WorkloadHeatmap)
- TaskDetailModal integrating Comments/Attachments
- UserSelect component for task assignment

## Medium Priority Issues (MED-001~012) - All Fixed
- MED-001~005: DB commits, N+1 queries, datetime, error format, blocker flag
- MED-006: Project health dashboard (HealthService, ProjectHealthPage)
- MED-007: Capacity update API (PUT /api/users/{id}/capacity)
- MED-008: Schedule triggers (cron parsing, deadline reminders)
- MED-009: Watermark feature (image/PDF watermarking)
- MED-010~012: useEffect deps, DOM operations, PDF export

## New Files
- backend/app/api/health/ - Project health API
- backend/app/services/health_service.py
- backend/app/services/trigger_scheduler.py
- backend/app/services/watermark_service.py
- backend/app/core/rate_limiter.py
- frontend/src/pages/ProjectHealthPage.tsx
- frontend/src/components/ProjectHealthCard.tsx
- frontend/src/components/KanbanBoard.tsx
- frontend/src/components/WorkloadHeatmap.tsx

## Tests
- 113 new tests passing (health: 32, users: 14, triggers: 35, watermark: 32)

## OpenSpec Archives
- add-project-health-dashboard
- add-capacity-update-api
- add-schedule-triggers
- add-watermark-feature
- add-rate-limiting
- enhance-frontend-ux
- add-resource-management-ui

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 21:49:52 +08:00

125 lines
4.8 KiB
Python

"""
Test suite for rate limiting functionality.
Tests the rate limiting feature on the login endpoint to ensure
protection against brute force attacks.
"""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
from app.services.auth_client import AuthAPIError
class TestRateLimiting:
"""Test rate limiting on the login endpoint."""
def test_login_rate_limit_exceeded(self, client):
"""
Test that the login endpoint returns 429 after exceeding rate limit.
GIVEN a client IP has made 5 login attempts within 1 minute
WHEN the client attempts another login
THEN the system returns HTTP 429 Too Many Requests
AND the response includes a Retry-After header
"""
# Mock the external auth service to return auth error
with patch("app.api.auth.router.verify_credentials", new_callable=AsyncMock) as mock_verify:
mock_verify.side_effect = AuthAPIError("Invalid credentials")
login_data = {"email": "test@example.com", "password": "wrongpassword"}
# Make 5 requests (the limit)
for i in range(5):
response = client.post("/api/auth/login", json=login_data)
# These should fail due to invalid credentials (401), but not rate limit
assert response.status_code == 401, f"Request {i+1} expected 401, got {response.status_code}"
# The 6th request should be rate limited
response = client.post("/api/auth/login", json=login_data)
assert response.status_code == 429, f"Expected 429 Too Many Requests, got {response.status_code}"
# Response should contain error details
data = response.json()
assert "error" in data or "detail" in data, "Response should contain error details"
def test_login_within_rate_limit(self, client):
"""
Test that requests within the rate limit are allowed.
GIVEN a client IP has not exceeded the rate limit
WHEN the client makes login requests
THEN the requests are processed normally (not rate limited)
"""
with patch("app.api.auth.router.verify_credentials", new_callable=AsyncMock) as mock_verify:
mock_verify.side_effect = AuthAPIError("Invalid credentials")
login_data = {"email": "test@example.com", "password": "wrongpassword"}
# Make requests within the limit
for i in range(3):
response = client.post("/api/auth/login", json=login_data)
# These should fail due to invalid credentials (401), but not be rate limited
assert response.status_code == 401, f"Request {i+1} expected 401, got {response.status_code}"
def test_rate_limit_response_format(self, client):
"""
Test that the 429 response format matches API standards.
GIVEN the rate limit has been exceeded
WHEN the client receives a 429 response
THEN the response body contains appropriate error information
"""
with patch("app.api.auth.router.verify_credentials", new_callable=AsyncMock) as mock_verify:
mock_verify.side_effect = AuthAPIError("Invalid credentials")
login_data = {"email": "test@example.com", "password": "wrongpassword"}
# Exhaust the rate limit
for _ in range(5):
client.post("/api/auth/login", json=login_data)
# The next request should be rate limited
response = client.post("/api/auth/login", json=login_data)
assert response.status_code == 429
# Check response body contains error information
data = response.json()
assert "error" in data or "detail" in data, "Response should contain error details"
class TestRateLimiterConfiguration:
"""Test rate limiter configuration."""
def test_limiter_uses_redis_storage(self):
"""
Test that the limiter is configured with Redis storage.
GIVEN the rate limiter configuration
WHEN we inspect the storage URI
THEN it should be configured to use Redis
"""
from app.core.rate_limiter import limiter
from app.core.config import settings
# The limiter should be configured
assert limiter is not None
# Verify Redis URL is properly configured
assert settings.REDIS_URL.startswith("redis://")
def test_limiter_uses_remote_address_key(self):
"""
Test that the limiter uses client IP as the key.
GIVEN the rate limiter configuration
WHEN we check the key function
THEN it should use get_remote_address
"""
from app.core.rate_limiter import limiter
from slowapi.util import get_remote_address
# The key function should be get_remote_address
assert limiter._key_func == get_remote_address