Add security regression tests for all hardening fixes
- New: test_security_headers.py — verify all headers present, HSTS conditional on HTTPS - New: test_passkey_error_handling.py — generic vs user-facing error messages - New: test_poi_validation.py — field length and coordinate range constraints - Extend test_rate_limiter.py — client IP depth selection, in-memory fallback enforcement - Extend test_models.py — sqm range validation - Extend test_task_service.py — IDOR 404, ownership 200, traceback suppression in production
This commit is contained in:
parent
727dd537ef
commit
492921424e
6 changed files with 365 additions and 0 deletions
|
|
@ -30,6 +30,7 @@ def _make_config(**overrides: object) -> RateLimitConfig:
|
|||
"geojson_stream_batch_size_cap": 200,
|
||||
"rate_limit_redis_db": 3,
|
||||
"metrics_allowed_ips": "127.0.0.1",
|
||||
"trusted_proxy_depth": 1,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return RateLimitConfig(**defaults) # type: ignore[arg-type]
|
||||
|
|
@ -236,3 +237,92 @@ class TestRateLimitConfig:
|
|||
# Should fall back to default
|
||||
assert config.endpoint_limits["/api/listing"].max_requests == 30
|
||||
assert config.endpoint_limits["/api/listing"].window_seconds == 60
|
||||
|
||||
|
||||
from api.rate_limiter import _client_ip
|
||||
|
||||
|
||||
class TestClientIp:
|
||||
"""Tests for _client_ip with trusted proxy depth."""
|
||||
|
||||
def test_uses_rightmost_ip_with_depth_1(self) -> None:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": [(b"x-forwarded-for", b"spoofed.ip, proxy1, real.ip")],
|
||||
}
|
||||
request = Request(scope)
|
||||
assert _client_ip(request, depth=1) == "real.ip"
|
||||
|
||||
def test_uses_second_from_right_with_depth_2(self) -> None:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": [(b"x-forwarded-for", b"spoofed.ip, real.ip, proxy1")],
|
||||
}
|
||||
request = Request(scope)
|
||||
assert _client_ip(request, depth=2) == "real.ip"
|
||||
|
||||
def test_falls_back_to_connection_ip(self) -> None:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": [],
|
||||
"client": ("192.168.1.1", 12345),
|
||||
}
|
||||
request = Request(scope)
|
||||
assert _client_ip(request) == "192.168.1.1"
|
||||
|
||||
def test_no_client_returns_unknown(self) -> None:
|
||||
scope = {"type": "http", "headers": []}
|
||||
request = Request(scope)
|
||||
assert _client_ip(request) == "unknown"
|
||||
|
||||
def test_single_ip_with_depth_1(self) -> None:
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": [(b"x-forwarded-for", b"single.ip")],
|
||||
}
|
||||
request = Request(scope)
|
||||
assert _client_ip(request, depth=1) == "single.ip"
|
||||
|
||||
|
||||
class TestInMemoryFallback:
|
||||
"""Tests for in-memory rate limit fallback when Redis is unavailable."""
|
||||
|
||||
@mock.patch("api.rate_limiter._get_rate_limit_redis")
|
||||
def test_fallback_enforces_limits_when_redis_unavailable(self, mock_get_redis: mock.MagicMock) -> None:
|
||||
"""When Redis is unavailable at startup, in-memory fallback should enforce limits."""
|
||||
import redis as redis_lib
|
||||
mock_get_redis.side_effect = redis_lib.RedisError("connection refused")
|
||||
|
||||
config = _make_config()
|
||||
app = _build_app(config)
|
||||
client = TestClient(app)
|
||||
|
||||
# Should allow first 3 requests (limit=3)
|
||||
for i in range(3):
|
||||
resp = client.get("/api/listing")
|
||||
assert resp.status_code == 200, f"Request {i+1} should be allowed"
|
||||
|
||||
# 4th request should be rate limited
|
||||
resp = client.get("/api/listing")
|
||||
assert resp.status_code == 429
|
||||
|
||||
@mock.patch("api.rate_limiter._get_rate_limit_redis")
|
||||
def test_fallback_activates_on_redis_error_during_request(self, mock_get_redis: mock.MagicMock) -> None:
|
||||
"""When Redis errors during request handling, fallback should activate."""
|
||||
import redis as redis_lib
|
||||
|
||||
mock_redis = mock.MagicMock()
|
||||
mock_pipe = mock.MagicMock()
|
||||
mock_pipe.execute.side_effect = redis_lib.RedisError("connection lost")
|
||||
mock_redis.pipeline.return_value = mock_pipe
|
||||
mock_redis.ping.return_value = True
|
||||
mock_get_redis.return_value = mock_redis
|
||||
|
||||
config = _make_config()
|
||||
app = _build_app(config)
|
||||
client = TestClient(app)
|
||||
|
||||
# First request should succeed via fallback
|
||||
resp = client.get("/api/listing")
|
||||
assert resp.status_code == 200
|
||||
assert "X-RateLimit-Limit" in resp.headers
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue