"""
Property-Based Tests for Security Features
Tests:
- Property 25: Rate Limiting Execution
- Property 26: Input Validation and Sanitization
Uses Hypothesis for property-based testing to verify security properties
across a wide range of inputs.
"""
import pytest
import time
from hypothesis import given, strategies as st, settings, assume, HealthCheck
from fastapi.testclient import TestClient
from src.main import app
from src.utils.validators import sanitize_string, sanitize_dict
from src.utils.errors import InvalidParameterException
# ============================================================================
# Property 25: Rate Limiting Execution
# ============================================================================
class TestRateLimitingProperties:
"""
Property 25: Rate Limiting Execution
Validates: Requirements 20.1, 20.2
For any request that exceeds the configured rate limit, the system should:
1. Reject the request with 429 status code
2. Include Retry-After header
3. Include rate limit headers (X-RateLimit-*)
4. Track requests per user and per IP
"""
def test_rate_limit_headers_present(self):
"""
Property: All responses should include rate limit headers.
**Validates: Requirements 20.2**
"""
client = TestClient(app)
response = client.get("/health")
# Verify rate limit headers are present
assert "X-RateLimit-Limit" in response.headers
assert "X-RateLimit-Remaining" in response.headers
assert "X-RateLimit-Reset" in response.headers
# Verify headers contain valid values
# Note: If Redis is not connected, limit may be 0 (rate limiting disabled)
limit = int(response.headers["X-RateLimit-Limit"])
remaining = int(response.headers["X-RateLimit-Remaining"])
reset_time = int(response.headers["X-RateLimit-Reset"])
# Headers should be present and parseable as integers
assert limit >= 0
assert remaining >= 0
assert reset_time >= 0
@given(
num_requests=st.integers(min_value=1, max_value=5)
)
@settings(max_examples=10, deadline=2000)
def test_rate_limit_headers_decrement(self, num_requests):
"""
Property: Rate limit remaining should decrement with each request.
**Validates: Requirements 20.1, 20.2**
"""
client = TestClient(app)
previous_remaining = None
for i in range(num_requests):
response = client.get("/health")
remaining = int(response.headers["X-RateLimit-Remaining"])
if previous_remaining is not None:
# Remaining should decrease (or stay same if limit is very high)
assert remaining <= previous_remaining
previous_remaining = remaining
# ============================================================================
# Property 26: Input Validation and Sanitization
# ============================================================================
class TestInputValidationProperties:
"""
Property 26: Input Validation and Sanitization
Validates: Requirements 20.3
For any user input, the system should:
1. Detect and reject SQL injection attempts
2. Detect and reject XSS attempts
3. Sanitize safe inputs appropriately
4. Preserve safe content while escaping dangerous content
"""
# SQL Injection test cases
@given(
sql_keyword=st.sampled_from([
"UNION SELECT", "DROP TABLE", "DELETE FROM", "INSERT INTO",
"UPDATE SET", "EXEC", "EXECUTE", "'; DROP", "admin'--",
"1' OR '1'='1", "1 UNION SELECT"
])
)
@settings(max_examples=30, deadline=2000)
def test_sql_injection_detection(self, sql_keyword):
"""
Property: Any input containing SQL injection patterns should be rejected.
**Validates: Requirements 20.3**
"""
# Create malicious input with SQL keyword
malicious_input = f"test {sql_keyword} malicious"
# Should raise InvalidParameterException
with pytest.raises(InvalidParameterException) as exc_info:
sanitize_string(malicious_input, "test_field")
# Verify exception was raised (the specific message may vary)
assert exc_info.value is not None
# XSS test cases
@given(
xss_pattern=st.sampled_from([
"",
"",
"javascript:alert('XSS')",
"",
"