feat: initial public release
ConsentOS — a privacy-first cookie consent management platform. Self-hosted, source-available alternative to OneTrust, Cookiebot, and CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant architecture with role-based access, configuration cascade (system → org → group → site → region), dark-pattern detection in the scanner, and a tamper-evident consent record audit trail. This is the initial public release. Prior development history is retained internally. See README.md for the feature list, architecture overview, and quick-start instructions. Licensed under the Elastic Licence 2.0 — self-host freely; do not resell as a managed service.
This commit is contained in:
137
apps/api/tests/test_middleware_rate_limit.py
Normal file
137
apps/api/tests/test_middleware_rate_limit.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Tests for the rate limiting middleware."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from src.middleware.rate_limit import RateLimitMiddleware
|
||||
|
||||
|
||||
class TestRateLimitMiddleware:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_ip_from_forwarded_for(self):
|
||||
from starlette.applications import Starlette
|
||||
|
||||
app = Starlette()
|
||||
middleware = RateLimitMiddleware(app)
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {"x-forwarded-for": "1.2.3.4, 5.6.7.8"}
|
||||
assert middleware._get_client_ip(request) == "1.2.3.4"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_ip_from_real_ip(self):
|
||||
from starlette.applications import Starlette
|
||||
|
||||
app = Starlette()
|
||||
middleware = RateLimitMiddleware(app)
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {"x-real-ip": "9.8.7.6"}
|
||||
assert middleware._get_client_ip(request) == "9.8.7.6"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_ip_from_client(self):
|
||||
from starlette.applications import Starlette
|
||||
|
||||
app = Starlette()
|
||||
middleware = RateLimitMiddleware(app)
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
request.client = MagicMock()
|
||||
request.client.host = "10.0.0.1"
|
||||
assert middleware._get_client_ip(request) == "10.0.0.1"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_client_ip_no_client(self):
|
||||
from starlette.applications import Starlette
|
||||
|
||||
app = Starlette()
|
||||
middleware = RateLimitMiddleware(app)
|
||||
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
request.client = None
|
||||
assert middleware._get_client_ip(request) == "unknown"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_bypasses_rate_limit(self):
|
||||
"""Health checks should never be rate limited."""
|
||||
from src.main import create_app
|
||||
|
||||
app = create_app()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passes_through_when_redis_unavailable(self):
|
||||
"""When Redis is down, requests should still be served."""
|
||||
from src.main import create_app
|
||||
|
||||
# Rate limiting disabled by default in test settings
|
||||
app = create_app()
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit_headers_present(self):
|
||||
"""Rate limit headers should be added when middleware is active."""
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/test")
|
||||
async def test_endpoint():
|
||||
return {"ok": True}
|
||||
|
||||
# Mock Redis
|
||||
mock_redis = AsyncMock()
|
||||
mock_redis.incr = AsyncMock(return_value=1)
|
||||
mock_redis.expire = AsyncMock()
|
||||
|
||||
middleware = RateLimitMiddleware(app, requests_per_minute=100)
|
||||
middleware._redis = mock_redis
|
||||
|
||||
# Since we can't easily inject the mock Redis into the ASGI middleware,
|
||||
# test the logic unit separately
|
||||
assert middleware.requests_per_minute == 100
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_middleware_creation(self):
|
||||
"""Middleware should initialise with provided parameters."""
|
||||
from starlette.applications import Starlette
|
||||
|
||||
app = Starlette()
|
||||
middleware = RateLimitMiddleware(app, redis_url="redis://fake:6379", requests_per_minute=30)
|
||||
assert middleware.requests_per_minute == 30
|
||||
assert middleware.redis_url == "redis://fake:6379"
|
||||
assert middleware._redis is None # Lazy initialisation
|
||||
|
||||
|
||||
class TestRateLimitConfiguration:
|
||||
def test_default_settings_enabled(self, monkeypatch):
|
||||
"""Rate limiting is on by default — public endpoints must not be DoS-able.
|
||||
|
||||
Note: the suite-wide conftest sets ``RATE_LIMIT_ENABLED=false``
|
||||
so other tests aren't rate-limited by Redis; we unset it here
|
||||
to verify the baked-in default.
|
||||
"""
|
||||
monkeypatch.delenv("RATE_LIMIT_ENABLED", raising=False)
|
||||
|
||||
from src.config.settings import Settings
|
||||
|
||||
settings = Settings()
|
||||
assert settings.rate_limit_enabled is True
|
||||
|
||||
def test_configurable_limit(self):
|
||||
"""Rate limit per minute should be configurable."""
|
||||
from src.config.settings import Settings
|
||||
|
||||
settings = Settings(rate_limit_per_minute=120)
|
||||
assert settings.rate_limit_per_minute == 120
|
||||
Reference in New Issue
Block a user