- Change file mode from 644 to 755 for all project files - Add chain_id parameter to get_balance RPC endpoint with default "ait-devnet" - Rename Miner.extra_meta_data to extra_metadata for consistency
303 lines
11 KiB
Python
Executable File
303 lines
11 KiB
Python
Executable File
"""
|
|
Focused test suite for rate limiting and error handling components
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
class TestRateLimitingComponents:
|
|
"""Test rate limiting components without full app import"""
|
|
|
|
def test_settings_rate_limit_configuration(self):
|
|
"""Test rate limit configuration in settings"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Verify all rate limit settings are present
|
|
rate_limit_attrs = [
|
|
'rate_limit_jobs_submit',
|
|
'rate_limit_miner_register',
|
|
'rate_limit_miner_heartbeat',
|
|
'rate_limit_admin_stats',
|
|
'rate_limit_marketplace_list',
|
|
'rate_limit_marketplace_stats',
|
|
'rate_limit_marketplace_bid',
|
|
'rate_limit_exchange_payment'
|
|
]
|
|
|
|
for attr in rate_limit_attrs:
|
|
assert hasattr(settings, attr), f"Missing rate limit configuration: {attr}"
|
|
value = getattr(settings, attr)
|
|
assert isinstance(value, str), f"Rate limit {attr} should be a string"
|
|
assert "/" in value, f"Rate limit {attr} should contain '/' (e.g., '100/minute')"
|
|
|
|
def test_rate_limit_default_values(self):
|
|
"""Test rate limit default values"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Verify default values
|
|
assert settings.rate_limit_jobs_submit == "100/minute"
|
|
assert settings.rate_limit_miner_register == "30/minute"
|
|
assert settings.rate_limit_miner_heartbeat == "60/minute"
|
|
assert settings.rate_limit_admin_stats == "20/minute"
|
|
assert settings.rate_limit_marketplace_list == "100/minute"
|
|
assert settings.rate_limit_marketplace_stats == "50/minute"
|
|
assert settings.rate_limit_marketplace_bid == "30/minute"
|
|
assert settings.rate_limit_exchange_payment == "20/minute"
|
|
|
|
def test_slowapi_import(self):
|
|
"""Test slowapi components can be imported"""
|
|
try:
|
|
from slowapi import Limiter
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
|
|
# Test limiter creation
|
|
limiter = Limiter(key_func=get_remote_address)
|
|
assert limiter is not None
|
|
|
|
# Test exception creation
|
|
exc = RateLimitExceeded("Test rate limit")
|
|
assert exc is not None
|
|
|
|
except ImportError as e:
|
|
pytest.fail(f"Failed to import slowapi components: {e}")
|
|
|
|
def test_rate_limit_decorator_creation(self):
|
|
"""Test rate limit decorator creation"""
|
|
try:
|
|
from slowapi import Limiter
|
|
from slowapi.util import get_remote_address
|
|
|
|
limiter = Limiter(key_func=get_remote_address)
|
|
|
|
# Test different rate limit strings
|
|
rate_limits = [
|
|
"100/minute",
|
|
"30/minute",
|
|
"20/minute",
|
|
"50/minute"
|
|
]
|
|
|
|
for rate_limit in rate_limits:
|
|
decorator = limiter.limit(rate_limit)
|
|
assert decorator is not None
|
|
|
|
except Exception as e:
|
|
pytest.fail(f"Failed to create rate limit decorators: {e}")
|
|
|
|
|
|
class TestErrorHandlingComponents:
|
|
"""Test error handling components without full app import"""
|
|
|
|
def test_error_response_model(self):
|
|
"""Test error response model structure"""
|
|
try:
|
|
from app.exceptions import ErrorResponse
|
|
|
|
error_response = ErrorResponse(
|
|
error={
|
|
"code": "TEST_ERROR",
|
|
"message": "Test error message",
|
|
"status": 400,
|
|
"details": [{
|
|
"field": "test_field",
|
|
"message": "Test detail",
|
|
"code": "test_code"
|
|
}]
|
|
},
|
|
request_id="test-123"
|
|
)
|
|
|
|
# Verify structure
|
|
assert error_response.error["code"] == "TEST_ERROR"
|
|
assert error_response.error["status"] == 400
|
|
assert error_response.request_id == "test-123"
|
|
assert len(error_response.error["details"]) == 1
|
|
|
|
# Test model dump
|
|
data = error_response.model_dump()
|
|
assert "error" in data
|
|
assert "request_id" in data
|
|
|
|
except ImportError as e:
|
|
pytest.fail(f"Failed to import ErrorResponse: {e}")
|
|
|
|
def test_429_error_response_structure(self):
|
|
"""Test 429 error response structure"""
|
|
try:
|
|
from app.exceptions import ErrorResponse
|
|
|
|
error_response = ErrorResponse(
|
|
error={
|
|
"code": "RATE_LIMIT_EXCEEDED",
|
|
"message": "Too many requests. Please try again later.",
|
|
"status": 429,
|
|
"details": [{
|
|
"field": "rate_limit",
|
|
"message": "100/minute",
|
|
"code": "too_many_requests",
|
|
"retry_after": 60
|
|
}]
|
|
},
|
|
request_id="req-123"
|
|
)
|
|
|
|
assert error_response.error["status"] == 429
|
|
assert error_response.error["code"] == "RATE_LIMIT_EXCEEDED"
|
|
assert "retry_after" in error_response.error["details"][0]
|
|
|
|
except ImportError as e:
|
|
pytest.fail(f"Failed to create 429 error response: {e}")
|
|
|
|
def test_validation_error_structure(self):
|
|
"""Test validation error response structure"""
|
|
try:
|
|
from app.exceptions import ErrorResponse
|
|
|
|
error_response = ErrorResponse(
|
|
error={
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "Request validation failed",
|
|
"status": 422,
|
|
"details": [{
|
|
"field": "test.field",
|
|
"message": "Field is required",
|
|
"code": "required"
|
|
}]
|
|
},
|
|
request_id="req-456"
|
|
)
|
|
|
|
assert error_response.error["status"] == 422
|
|
assert error_response.error["code"] == "VALIDATION_ERROR"
|
|
|
|
detail = error_response.error["details"][0]
|
|
assert detail["field"] == "test.field"
|
|
assert detail["code"] == "required"
|
|
|
|
except ImportError as e:
|
|
pytest.fail(f"Failed to create validation error response: {e}")
|
|
|
|
|
|
class TestConfigurationValidation:
|
|
"""Test configuration validation for rate limiting"""
|
|
|
|
def test_rate_limit_format_validation(self):
|
|
"""Test rate limit format validation"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Test valid formats
|
|
valid_formats = [
|
|
"100/minute",
|
|
"30/minute",
|
|
"20/minute",
|
|
"50/minute",
|
|
"100/hour",
|
|
"1000/day"
|
|
]
|
|
|
|
for rate_limit in valid_formats:
|
|
assert "/" in rate_limit, f"Rate limit {rate_limit} should contain '/'"
|
|
parts = rate_limit.split("/")
|
|
assert len(parts) == 2, f"Rate limit {rate_limit} should have format 'number/period'"
|
|
assert parts[0].isdigit(), f"Rate limit {rate_limit} should start with number"
|
|
|
|
def test_environment_based_configuration(self):
|
|
"""Test environment-based configuration"""
|
|
from app.config import Settings
|
|
|
|
# Test development environment
|
|
with patch.dict('os.environ', {'APP_ENV': 'dev'}):
|
|
settings = Settings(app_env="dev")
|
|
assert settings.app_env == "dev"
|
|
assert settings.rate_limit_jobs_submit == "100/minute"
|
|
|
|
# Test production environment
|
|
with patch.dict('os.environ', {'APP_ENV': 'production'}):
|
|
settings = Settings(app_env="production")
|
|
assert settings.app_env == "production"
|
|
assert settings.rate_limit_jobs_submit == "100/minute"
|
|
|
|
|
|
class TestLoggingIntegration:
|
|
"""Test logging integration for rate limiting and errors"""
|
|
|
|
def test_shared_logging_import(self):
|
|
"""Test shared logging import"""
|
|
try:
|
|
from aitbc.logging import get_logger
|
|
|
|
logger = get_logger("test")
|
|
assert logger is not None
|
|
assert hasattr(logger, 'info')
|
|
assert hasattr(logger, 'warning')
|
|
assert hasattr(logger, 'error')
|
|
|
|
except ImportError as e:
|
|
pytest.fail(f"Failed to import shared logging: {e}")
|
|
|
|
def test_audit_log_configuration(self):
|
|
"""Test audit log configuration"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Verify audit log directory configuration
|
|
assert hasattr(settings, 'audit_log_dir')
|
|
assert isinstance(settings.audit_log_dir, str)
|
|
assert len(settings.audit_log_dir) > 0
|
|
|
|
|
|
class TestRateLimitTierStrategy:
|
|
"""Test rate limit tier strategy"""
|
|
|
|
def test_tiered_rate_limits(self):
|
|
"""Test tiered rate limit strategy"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Verify tiered approach: financial operations have stricter limits
|
|
assert int(settings.rate_limit_exchange_payment.split("/")[0]) < int(settings.rate_limit_marketplace_list.split("/")[0])
|
|
assert int(settings.rate_limit_marketplace_bid.split("/")[0]) < int(settings.rate_limit_marketplace_list.split("/")[0])
|
|
assert int(settings.rate_limit_admin_stats.split("/")[0]) < int(settings.rate_limit_marketplace_list.split("/")[0])
|
|
|
|
# Verify reasonable limits for different operations
|
|
jobs_submit = int(settings.rate_limit_jobs_submit.split("/")[0])
|
|
miner_heartbeat = int(settings.rate_limit_miner_heartbeat.split("/")[0])
|
|
marketplace_list = int(settings.rate_limit_marketplace_list.split("/")[0])
|
|
|
|
assert jobs_submit >= 50, "Job submission should allow reasonable rate"
|
|
assert miner_heartbeat >= 30, "Miner heartbeat should allow reasonable rate"
|
|
assert marketplace_list >= 50, "Marketplace browsing should allow reasonable rate"
|
|
|
|
def test_security_focused_limits(self):
|
|
"""Test security-focused rate limits"""
|
|
from app.config import Settings
|
|
|
|
settings = Settings()
|
|
|
|
# Financial operations should have strictest limits
|
|
exchange_payment = int(settings.rate_limit_exchange_payment.split("/")[0])
|
|
marketplace_bid = int(settings.rate_limit_marketplace_bid.split("/")[0])
|
|
admin_stats = int(settings.rate_limit_admin_stats.split("/")[0])
|
|
|
|
# Exchange payment should be most restrictive
|
|
assert exchange_payment <= marketplace_bid
|
|
assert exchange_payment <= admin_stats
|
|
|
|
# All should be reasonable for security
|
|
assert exchange_payment <= 30, "Exchange payment should be rate limited for security"
|
|
assert marketplace_bid <= 50, "Marketplace bid should be rate limited for security"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|