mcp-security-framework 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_security_framework/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,523 @@
|
|
1
|
+
"""
|
2
|
+
FastAPI Security Middleware Tests
|
3
|
+
|
4
|
+
This module provides comprehensive unit tests for the FastAPISecurityMiddleware
|
5
|
+
class and its FastAPI-specific functionality.
|
6
|
+
|
7
|
+
Test Coverage:
|
8
|
+
- FastAPISecurityMiddleware initialization
|
9
|
+
- FastAPI request processing
|
10
|
+
- FastAPI authentication methods
|
11
|
+
- FastAPI response creation
|
12
|
+
- FastAPI error handling
|
13
|
+
- FastAPI header management
|
14
|
+
- FastAPI rate limiting integration
|
15
|
+
- FastAPI-specific request/response handling
|
16
|
+
|
17
|
+
Author: MCP Security Team
|
18
|
+
Version: 1.0.0
|
19
|
+
License: MIT
|
20
|
+
"""
|
21
|
+
|
22
|
+
import pytest
|
23
|
+
from unittest.mock import Mock, patch, AsyncMock, MagicMock
|
24
|
+
from typing import Dict, List, Any
|
25
|
+
|
26
|
+
from fastapi import Request, Response, HTTPException, status
|
27
|
+
from fastapi.responses import JSONResponse
|
28
|
+
|
29
|
+
from mcp_security_framework.middleware.fastapi_middleware import (
|
30
|
+
FastAPISecurityMiddleware,
|
31
|
+
FastAPIMiddlewareError
|
32
|
+
)
|
33
|
+
from mcp_security_framework.core.security_manager import SecurityManager
|
34
|
+
from mcp_security_framework.schemas.config import SecurityConfig, AuthConfig, RateLimitConfig
|
35
|
+
from mcp_security_framework.schemas.models import AuthResult, ValidationResult, ValidationStatus, AuthStatus, AuthMethod
|
36
|
+
|
37
|
+
|
38
|
+
class TestFastAPISecurityMiddleware:
|
39
|
+
"""Test suite for FastAPISecurityMiddleware class."""
|
40
|
+
|
41
|
+
def setup_method(self):
|
42
|
+
"""Set up test fixtures before each test method."""
|
43
|
+
# Create mock security manager
|
44
|
+
self.mock_security_manager = Mock(spec=SecurityManager)
|
45
|
+
self.mock_security_manager.config = SecurityConfig(
|
46
|
+
auth=AuthConfig(
|
47
|
+
enabled=True,
|
48
|
+
methods=["api_key", "jwt"],
|
49
|
+
public_paths=["/health", "/docs"],
|
50
|
+
jwt_secret="test_jwt_secret_key"
|
51
|
+
),
|
52
|
+
rate_limit=RateLimitConfig(
|
53
|
+
enabled=True,
|
54
|
+
default_requests_per_minute=100,
|
55
|
+
window_size_seconds=60
|
56
|
+
)
|
57
|
+
)
|
58
|
+
|
59
|
+
# Create mock auth manager
|
60
|
+
self.mock_auth_manager = Mock()
|
61
|
+
self.mock_security_manager.auth_manager = self.mock_auth_manager
|
62
|
+
|
63
|
+
# Setup rate_limiter mock
|
64
|
+
self.mock_security_manager.rate_limiter = Mock()
|
65
|
+
|
66
|
+
# Create middleware instance
|
67
|
+
self.middleware = FastAPISecurityMiddleware(self.mock_security_manager)
|
68
|
+
|
69
|
+
def create_mock_request(self, path: str = "/api/test", headers: Dict[str, str] = None) -> Mock:
|
70
|
+
"""Create a mock FastAPI request for testing."""
|
71
|
+
mock_request = Mock(spec=Request)
|
72
|
+
mock_request.url.path = path
|
73
|
+
mock_request.method = "GET"
|
74
|
+
mock_request.headers = headers or {}
|
75
|
+
mock_request.client = Mock()
|
76
|
+
mock_request.client.host = "127.0.0.1"
|
77
|
+
return mock_request
|
78
|
+
|
79
|
+
def test_initialization_success(self):
|
80
|
+
"""Test successful FastAPI middleware initialization."""
|
81
|
+
assert isinstance(self.middleware, FastAPISecurityMiddleware)
|
82
|
+
assert self.middleware.security_manager == self.mock_security_manager
|
83
|
+
assert self.middleware.config == self.mock_security_manager.config
|
84
|
+
|
85
|
+
@pytest.mark.asyncio
|
86
|
+
async def test_call_success(self):
|
87
|
+
"""Test successful middleware call."""
|
88
|
+
mock_request = self.create_mock_request()
|
89
|
+
mock_call_next = AsyncMock()
|
90
|
+
mock_response = Mock(spec=Response)
|
91
|
+
mock_response.status_code = 200
|
92
|
+
mock_response.headers = {}
|
93
|
+
mock_call_next.return_value = mock_response
|
94
|
+
|
95
|
+
# Mock successful authentication and authorization
|
96
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
97
|
+
|
98
|
+
# Mock the _authenticate_request method directly
|
99
|
+
auth_result = AuthResult(
|
100
|
+
is_valid=True,
|
101
|
+
status=AuthStatus.SUCCESS,
|
102
|
+
username="test_user",
|
103
|
+
roles=["user"],
|
104
|
+
auth_method=AuthMethod.API_KEY
|
105
|
+
)
|
106
|
+
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
107
|
+
|
108
|
+
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
109
|
+
is_valid=True,
|
110
|
+
status=ValidationStatus.VALID
|
111
|
+
)
|
112
|
+
|
113
|
+
result = await self.middleware(mock_request, mock_call_next)
|
114
|
+
|
115
|
+
assert result == mock_response
|
116
|
+
mock_call_next.assert_called_once_with(mock_request)
|
117
|
+
|
118
|
+
@pytest.mark.asyncio
|
119
|
+
async def test_call_rate_limit_exceeded(self):
|
120
|
+
"""Test middleware call with rate limit exceeded."""
|
121
|
+
mock_request = self.create_mock_request()
|
122
|
+
mock_call_next = AsyncMock()
|
123
|
+
|
124
|
+
# Mock _is_public_path to return False so rate limiting is checked
|
125
|
+
self.middleware._is_public_path = Mock(return_value=False)
|
126
|
+
# Mock the security manager's check_rate_limit method
|
127
|
+
self.mock_security_manager.check_rate_limit.return_value = False
|
128
|
+
|
129
|
+
result = await self.middleware(mock_request, mock_call_next)
|
130
|
+
|
131
|
+
assert isinstance(result, JSONResponse)
|
132
|
+
assert result.status_code == status.HTTP_429_TOO_MANY_REQUESTS
|
133
|
+
assert "Rate limit exceeded" in result.body.decode()
|
134
|
+
mock_call_next.assert_not_called()
|
135
|
+
|
136
|
+
@pytest.mark.asyncio
|
137
|
+
async def test_call_public_path(self):
|
138
|
+
"""Test middleware call with public path."""
|
139
|
+
mock_request = self.create_mock_request(path="/health")
|
140
|
+
mock_call_next = AsyncMock()
|
141
|
+
mock_response = Mock(spec=Response)
|
142
|
+
mock_response.status_code = 200
|
143
|
+
mock_response.headers = {}
|
144
|
+
mock_call_next.return_value = mock_response
|
145
|
+
|
146
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
147
|
+
|
148
|
+
result = await self.middleware(mock_request, mock_call_next)
|
149
|
+
|
150
|
+
assert result == mock_response
|
151
|
+
mock_call_next.assert_called_once_with(mock_request)
|
152
|
+
|
153
|
+
@pytest.mark.asyncio
|
154
|
+
async def test_call_authentication_failed(self):
|
155
|
+
"""Test middleware call with authentication failure."""
|
156
|
+
mock_request = self.create_mock_request()
|
157
|
+
mock_call_next = AsyncMock()
|
158
|
+
|
159
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
160
|
+
self.mock_auth_manager.authenticate_api_key.return_value = AuthResult(
|
161
|
+
is_valid=False,
|
162
|
+
status=AuthStatus.FAILED,
|
163
|
+
username=None,
|
164
|
+
roles=[],
|
165
|
+
auth_method=AuthMethod.API_KEY,
|
166
|
+
error_code=-32005,
|
167
|
+
error_message="Authentication failed"
|
168
|
+
)
|
169
|
+
|
170
|
+
result = await self.middleware(mock_request, mock_call_next)
|
171
|
+
|
172
|
+
assert isinstance(result, JSONResponse)
|
173
|
+
assert result.status_code == status.HTTP_401_UNAUTHORIZED
|
174
|
+
assert "Authentication failed" in result.body.decode()
|
175
|
+
mock_call_next.assert_not_called()
|
176
|
+
|
177
|
+
@pytest.mark.asyncio
|
178
|
+
async def test_call_permission_denied(self):
|
179
|
+
"""Test middleware call with permission denied."""
|
180
|
+
mock_request = self.create_mock_request()
|
181
|
+
mock_call_next = AsyncMock()
|
182
|
+
|
183
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
184
|
+
|
185
|
+
# Mock successful authentication
|
186
|
+
auth_result = AuthResult(
|
187
|
+
is_valid=True,
|
188
|
+
status=AuthStatus.SUCCESS,
|
189
|
+
username="test_user",
|
190
|
+
roles=["user"],
|
191
|
+
auth_method=AuthMethod.API_KEY
|
192
|
+
)
|
193
|
+
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
194
|
+
|
195
|
+
# Mock the _validate_permissions method directly
|
196
|
+
self.middleware._validate_permissions = Mock(return_value=False)
|
197
|
+
|
198
|
+
result = await self.middleware(mock_request, mock_call_next)
|
199
|
+
|
200
|
+
assert isinstance(result, JSONResponse)
|
201
|
+
assert result.status_code == status.HTTP_403_FORBIDDEN
|
202
|
+
assert "Permission denied" in result.body.decode()
|
203
|
+
mock_call_next.assert_not_called()
|
204
|
+
|
205
|
+
def test_get_rate_limit_identifier(self):
|
206
|
+
"""Test getting rate limit identifier from request."""
|
207
|
+
mock_request = self.create_mock_request()
|
208
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
209
|
+
assert result == "127.0.0.1"
|
210
|
+
|
211
|
+
def test_get_rate_limit_identifier_with_forwarded_for(self):
|
212
|
+
"""Test getting rate limit identifier with X-Forwarded-For header."""
|
213
|
+
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
214
|
+
mock_request = self.create_mock_request(headers=headers)
|
215
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
216
|
+
assert result == "192.168.1.1"
|
217
|
+
|
218
|
+
def test_get_rate_limit_identifier_with_real_ip(self):
|
219
|
+
"""Test getting rate limit identifier with X-Real-IP header."""
|
220
|
+
headers = {"X-Real-IP": "192.168.1.100"}
|
221
|
+
mock_request = self.create_mock_request(headers=headers)
|
222
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
223
|
+
assert result == "192.168.1.100"
|
224
|
+
|
225
|
+
def test_get_request_path(self):
|
226
|
+
"""Test getting request path from FastAPI request."""
|
227
|
+
mock_request = self.create_mock_request(path="/api/users")
|
228
|
+
result = self.middleware._get_request_path(mock_request)
|
229
|
+
assert result == "/api/users"
|
230
|
+
|
231
|
+
def test_get_required_permissions_from_state(self):
|
232
|
+
"""Test getting required permissions from request state."""
|
233
|
+
mock_request = self.create_mock_request()
|
234
|
+
mock_request.state.required_permissions = ["read", "write"]
|
235
|
+
|
236
|
+
result = self.middleware._get_required_permissions(mock_request)
|
237
|
+
assert result == ["read", "write"]
|
238
|
+
|
239
|
+
def test_get_required_permissions_default(self):
|
240
|
+
"""Test getting required permissions when not set."""
|
241
|
+
mock_request = self.create_mock_request()
|
242
|
+
# Ensure state doesn't have required_permissions
|
243
|
+
if hasattr(mock_request, 'state'):
|
244
|
+
delattr(mock_request.state, 'required_permissions')
|
245
|
+
result = self.middleware._get_required_permissions(mock_request)
|
246
|
+
assert result == []
|
247
|
+
|
248
|
+
@pytest.mark.asyncio
|
249
|
+
async def test_try_api_key_auth_success(self):
|
250
|
+
"""Test successful API key authentication."""
|
251
|
+
headers = {"X-API-Key": "valid_key"}
|
252
|
+
mock_request = self.create_mock_request(headers=headers)
|
253
|
+
|
254
|
+
expected_result = AuthResult(
|
255
|
+
is_valid=True,
|
256
|
+
status=AuthStatus.SUCCESS,
|
257
|
+
username="test_user",
|
258
|
+
roles=["user"],
|
259
|
+
auth_method=AuthMethod.API_KEY
|
260
|
+
)
|
261
|
+
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
262
|
+
|
263
|
+
result = await self.middleware._try_api_key_auth(mock_request)
|
264
|
+
|
265
|
+
assert result == expected_result
|
266
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("valid_key")
|
267
|
+
|
268
|
+
@pytest.mark.asyncio
|
269
|
+
async def test_try_api_key_auth_from_authorization_header(self):
|
270
|
+
"""Test API key authentication from Authorization header."""
|
271
|
+
headers = {"Authorization": "Bearer api_key_123"}
|
272
|
+
mock_request = self.create_mock_request(headers=headers)
|
273
|
+
|
274
|
+
expected_result = AuthResult(
|
275
|
+
is_valid=True,
|
276
|
+
status=AuthStatus.SUCCESS,
|
277
|
+
username="test_user",
|
278
|
+
roles=["user"],
|
279
|
+
auth_method=AuthMethod.API_KEY
|
280
|
+
)
|
281
|
+
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
282
|
+
|
283
|
+
result = await self.middleware._try_api_key_auth(mock_request)
|
284
|
+
|
285
|
+
assert result == expected_result
|
286
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("api_key_123")
|
287
|
+
|
288
|
+
@pytest.mark.asyncio
|
289
|
+
async def test_try_api_key_auth_no_key(self):
|
290
|
+
"""Test API key authentication with no key provided."""
|
291
|
+
mock_request = self.create_mock_request()
|
292
|
+
|
293
|
+
result = await self.middleware._try_api_key_auth(mock_request)
|
294
|
+
|
295
|
+
assert result.is_valid is False
|
296
|
+
assert result.error_code == -32012
|
297
|
+
assert "API key not found" in result.error_message
|
298
|
+
|
299
|
+
@pytest.mark.asyncio
|
300
|
+
async def test_try_jwt_auth_success(self):
|
301
|
+
"""Test successful JWT authentication."""
|
302
|
+
headers = {"Authorization": "Bearer jwt_token_123"}
|
303
|
+
mock_request = self.create_mock_request(headers=headers)
|
304
|
+
|
305
|
+
expected_result = AuthResult(
|
306
|
+
is_valid=True,
|
307
|
+
status=AuthStatus.SUCCESS,
|
308
|
+
username="test_user",
|
309
|
+
roles=["user"],
|
310
|
+
auth_method=AuthMethod.JWT
|
311
|
+
)
|
312
|
+
self.mock_auth_manager.authenticate_jwt_token.return_value = expected_result
|
313
|
+
|
314
|
+
result = await self.middleware._try_jwt_auth(mock_request)
|
315
|
+
|
316
|
+
assert result == expected_result
|
317
|
+
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with("jwt_token_123")
|
318
|
+
|
319
|
+
@pytest.mark.asyncio
|
320
|
+
async def test_try_jwt_auth_no_token(self):
|
321
|
+
"""Test JWT authentication with no token provided."""
|
322
|
+
mock_request = self.create_mock_request()
|
323
|
+
|
324
|
+
result = await self.middleware._try_jwt_auth(mock_request)
|
325
|
+
|
326
|
+
assert result.is_valid is False
|
327
|
+
assert result.error_code == -32013
|
328
|
+
assert "JWT token not found" in result.error_message
|
329
|
+
|
330
|
+
@pytest.mark.asyncio
|
331
|
+
async def test_try_jwt_auth_invalid_header(self):
|
332
|
+
"""Test JWT authentication with invalid Authorization header."""
|
333
|
+
headers = {"Authorization": "Invalid jwt_token_123"}
|
334
|
+
mock_request = self.create_mock_request(headers=headers)
|
335
|
+
|
336
|
+
result = await self.middleware._try_jwt_auth(mock_request)
|
337
|
+
|
338
|
+
assert result.is_valid is False
|
339
|
+
assert result.error_code == -32013
|
340
|
+
assert "JWT token not found" in result.error_message
|
341
|
+
|
342
|
+
@pytest.mark.asyncio
|
343
|
+
async def test_try_certificate_auth_not_implemented(self):
|
344
|
+
"""Test certificate authentication (not implemented)."""
|
345
|
+
mock_request = self.create_mock_request()
|
346
|
+
|
347
|
+
result = await self.middleware._try_certificate_auth(mock_request)
|
348
|
+
|
349
|
+
assert result.is_valid is False
|
350
|
+
assert result.error_code == -32014
|
351
|
+
assert "not implemented" in result.error_message
|
352
|
+
|
353
|
+
@pytest.mark.asyncio
|
354
|
+
async def test_try_basic_auth_not_implemented(self):
|
355
|
+
"""Test basic authentication (not implemented)."""
|
356
|
+
mock_request = self.create_mock_request()
|
357
|
+
|
358
|
+
result = await self.middleware._try_basic_auth(mock_request)
|
359
|
+
|
360
|
+
assert result.is_valid is False
|
361
|
+
assert result.error_code == -32015
|
362
|
+
assert "not found" in result.error_message
|
363
|
+
|
364
|
+
@pytest.mark.asyncio
|
365
|
+
async def test_try_auth_method_unsupported(self):
|
366
|
+
"""Test authentication with unsupported method."""
|
367
|
+
mock_request = self.create_mock_request()
|
368
|
+
|
369
|
+
result = await self.middleware._try_auth_method(mock_request, "unsupported_method")
|
370
|
+
|
371
|
+
assert result.is_valid is False
|
372
|
+
assert result.error_code == -32010
|
373
|
+
assert "Unsupported authentication method" in result.error_message
|
374
|
+
|
375
|
+
def test_apply_security_headers(self):
|
376
|
+
"""Test applying security headers to FastAPI response."""
|
377
|
+
mock_response = Mock(spec=Response)
|
378
|
+
mock_response.headers = {}
|
379
|
+
|
380
|
+
headers = {
|
381
|
+
"X-Content-Type-Options": "nosniff",
|
382
|
+
"X-Frame-Options": "DENY",
|
383
|
+
"X-XSS-Protection": "1; mode=block"
|
384
|
+
}
|
385
|
+
|
386
|
+
self.middleware._apply_security_headers(mock_response, headers)
|
387
|
+
|
388
|
+
assert mock_response.headers["X-Content-Type-Options"] == "nosniff"
|
389
|
+
assert mock_response.headers["X-Frame-Options"] == "DENY"
|
390
|
+
assert mock_response.headers["X-XSS-Protection"] == "1; mode=block"
|
391
|
+
|
392
|
+
def test_create_error_response(self):
|
393
|
+
"""Test creating error response."""
|
394
|
+
result = self.middleware._create_error_response(400, "Bad request")
|
395
|
+
|
396
|
+
assert isinstance(result, JSONResponse)
|
397
|
+
assert result.status_code == 400
|
398
|
+
assert "Security violation" in result.body.decode()
|
399
|
+
assert "Bad request" in result.body.decode()
|
400
|
+
|
401
|
+
def test_rate_limit_response(self):
|
402
|
+
"""Test creating rate limit response."""
|
403
|
+
result = self.middleware._rate_limit_response()
|
404
|
+
|
405
|
+
assert isinstance(result, JSONResponse)
|
406
|
+
assert result.status_code == status.HTTP_429_TOO_MANY_REQUESTS
|
407
|
+
assert "Rate limit exceeded" in result.body.decode()
|
408
|
+
assert "Retry-After" in result.headers
|
409
|
+
|
410
|
+
def test_auth_error_response(self):
|
411
|
+
"""Test creating authentication error response."""
|
412
|
+
auth_result = AuthResult(
|
413
|
+
is_valid=False,
|
414
|
+
status=AuthStatus.FAILED,
|
415
|
+
username=None,
|
416
|
+
roles=[],
|
417
|
+
auth_method=AuthMethod.API_KEY,
|
418
|
+
error_code=-32005,
|
419
|
+
error_message="Invalid API key"
|
420
|
+
)
|
421
|
+
|
422
|
+
result = self.middleware._auth_error_response(auth_result)
|
423
|
+
|
424
|
+
assert isinstance(result, JSONResponse)
|
425
|
+
assert result.status_code == status.HTTP_401_UNAUTHORIZED
|
426
|
+
assert "Authentication failed" in result.body.decode()
|
427
|
+
assert "Invalid API key" in result.body.decode()
|
428
|
+
assert "WWW-Authenticate" in result.headers
|
429
|
+
|
430
|
+
def test_permission_error_response(self):
|
431
|
+
"""Test creating permission error response."""
|
432
|
+
result = self.middleware._permission_error_response()
|
433
|
+
|
434
|
+
assert isinstance(result, JSONResponse)
|
435
|
+
assert result.status_code == status.HTTP_403_FORBIDDEN
|
436
|
+
assert "Permission denied" in result.body.decode()
|
437
|
+
assert "Insufficient permissions" in result.body.decode()
|
438
|
+
|
439
|
+
def test_get_client_ip_from_forwarded_for(self):
|
440
|
+
"""Test getting client IP from X-Forwarded-For header."""
|
441
|
+
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
442
|
+
mock_request = self.create_mock_request(headers=headers)
|
443
|
+
|
444
|
+
result = self.middleware._get_client_ip(mock_request)
|
445
|
+
assert result == "192.168.1.1"
|
446
|
+
|
447
|
+
def test_get_client_ip_from_real_ip(self):
|
448
|
+
"""Test getting client IP from X-Real-IP header."""
|
449
|
+
headers = {"X-Real-IP": "192.168.1.100"}
|
450
|
+
mock_request = self.create_mock_request(headers=headers)
|
451
|
+
|
452
|
+
result = self.middleware._get_client_ip(mock_request)
|
453
|
+
assert result == "192.168.1.100"
|
454
|
+
|
455
|
+
def test_get_client_ip_from_client_host(self):
|
456
|
+
"""Test getting client IP from client host."""
|
457
|
+
mock_request = self.create_mock_request()
|
458
|
+
mock_request.client.host = "192.168.1.50"
|
459
|
+
|
460
|
+
result = self.middleware._get_client_ip(mock_request)
|
461
|
+
assert result == "192.168.1.50"
|
462
|
+
|
463
|
+
def test_get_client_ip_fallback(self):
|
464
|
+
"""Test getting client IP with fallback."""
|
465
|
+
mock_request = self.create_mock_request()
|
466
|
+
mock_request.client = None
|
467
|
+
|
468
|
+
result = self.middleware._get_client_ip(mock_request)
|
469
|
+
assert result == "127.0.0.1"
|
470
|
+
|
471
|
+
@pytest.mark.asyncio
|
472
|
+
async def test_call_with_http_exception(self):
|
473
|
+
"""Test middleware call that raises HTTPException."""
|
474
|
+
mock_request = self.create_mock_request()
|
475
|
+
mock_call_next = AsyncMock()
|
476
|
+
mock_call_next.side_effect = HTTPException(status_code=500, detail="Internal error")
|
477
|
+
|
478
|
+
# Mock successful authentication to reach call_next
|
479
|
+
auth_result = AuthResult(
|
480
|
+
is_valid=True,
|
481
|
+
status=AuthStatus.SUCCESS,
|
482
|
+
username="test_user",
|
483
|
+
roles=["user"],
|
484
|
+
auth_method=AuthMethod.API_KEY
|
485
|
+
)
|
486
|
+
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
487
|
+
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
488
|
+
is_valid=True,
|
489
|
+
status=ValidationStatus.VALID
|
490
|
+
)
|
491
|
+
|
492
|
+
with pytest.raises(HTTPException) as exc_info:
|
493
|
+
await self.middleware(mock_request, mock_call_next)
|
494
|
+
|
495
|
+
assert exc_info.value.status_code == 500
|
496
|
+
assert exc_info.value.detail == "Internal error"
|
497
|
+
|
498
|
+
@pytest.mark.asyncio
|
499
|
+
async def test_call_with_general_exception(self):
|
500
|
+
"""Test middleware call with general exception."""
|
501
|
+
mock_request = self.create_mock_request()
|
502
|
+
mock_call_next = AsyncMock()
|
503
|
+
mock_call_next.side_effect = Exception("General error")
|
504
|
+
|
505
|
+
# Mock successful authentication to reach call_next
|
506
|
+
auth_result = AuthResult(
|
507
|
+
is_valid=True,
|
508
|
+
status=AuthStatus.SUCCESS,
|
509
|
+
username="test_user",
|
510
|
+
roles=["user"],
|
511
|
+
auth_method=AuthMethod.API_KEY
|
512
|
+
)
|
513
|
+
self.middleware._authenticate_request = AsyncMock(return_value=auth_result)
|
514
|
+
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
515
|
+
is_valid=True,
|
516
|
+
status=ValidationStatus.VALID
|
517
|
+
)
|
518
|
+
|
519
|
+
with pytest.raises(FastAPIMiddlewareError) as exc_info:
|
520
|
+
await self.middleware(mock_request, mock_call_next)
|
521
|
+
|
522
|
+
assert "Middleware processing failed" in str(exc_info.value)
|
523
|
+
assert exc_info.value.error_code == -32003
|