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,582 @@
|
|
1
|
+
"""
|
2
|
+
Flask Security Middleware Tests
|
3
|
+
|
4
|
+
This module provides comprehensive unit tests for the FlaskSecurityMiddleware
|
5
|
+
class and its Flask-specific functionality.
|
6
|
+
|
7
|
+
Test Coverage:
|
8
|
+
- FlaskSecurityMiddleware initialization
|
9
|
+
- Flask request processing
|
10
|
+
- Flask authentication methods
|
11
|
+
- Flask response creation
|
12
|
+
- Flask error handling
|
13
|
+
- Flask header management
|
14
|
+
- Flask rate limiting integration
|
15
|
+
- Flask-specific request/response handling
|
16
|
+
|
17
|
+
Author: MCP Security Team
|
18
|
+
Version: 1.0.0
|
19
|
+
License: MIT
|
20
|
+
"""
|
21
|
+
|
22
|
+
import pytest
|
23
|
+
import json
|
24
|
+
from unittest.mock import Mock, patch, MagicMock
|
25
|
+
from typing import Dict, List, Any
|
26
|
+
|
27
|
+
from flask import Request, Response
|
28
|
+
|
29
|
+
from mcp_security_framework.middleware.flask_middleware import (
|
30
|
+
FlaskSecurityMiddleware,
|
31
|
+
FlaskMiddlewareError
|
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 TestFlaskSecurityMiddleware:
|
39
|
+
"""Test suite for FlaskSecurityMiddleware 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 = FlaskSecurityMiddleware(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 Flask request for testing."""
|
71
|
+
mock_request = Mock(spec=Request)
|
72
|
+
mock_request.path = path
|
73
|
+
mock_request.method = "GET"
|
74
|
+
mock_request.headers = headers or {}
|
75
|
+
mock_request.remote_addr = "127.0.0.1"
|
76
|
+
return mock_request
|
77
|
+
|
78
|
+
def create_mock_environ(self, path: str = "/api/test", headers: Dict[str, str] = None) -> Dict[str, Any]:
|
79
|
+
"""Create a mock WSGI environ for testing."""
|
80
|
+
environ = {
|
81
|
+
'REQUEST_METHOD': 'GET',
|
82
|
+
'PATH_INFO': path,
|
83
|
+
'QUERY_STRING': '',
|
84
|
+
'SERVER_NAME': 'localhost',
|
85
|
+
'SERVER_PORT': '5000',
|
86
|
+
'HTTP_HOST': 'localhost:5000',
|
87
|
+
'wsgi.url_scheme': 'http',
|
88
|
+
'wsgi.input': Mock(),
|
89
|
+
'wsgi.errors': Mock(),
|
90
|
+
'wsgi.version': (1, 0),
|
91
|
+
'wsgi.run_once': False,
|
92
|
+
'wsgi.multithread': False,
|
93
|
+
'wsgi.multiprocess': False,
|
94
|
+
}
|
95
|
+
|
96
|
+
# Add headers to environ
|
97
|
+
if headers:
|
98
|
+
for key, value in headers.items():
|
99
|
+
environ[f'HTTP_{key.upper().replace("-", "_")}'] = value
|
100
|
+
|
101
|
+
return environ
|
102
|
+
|
103
|
+
def test_initialization_success(self):
|
104
|
+
"""Test successful Flask middleware initialization."""
|
105
|
+
assert isinstance(self.middleware, FlaskSecurityMiddleware)
|
106
|
+
assert self.middleware.security_manager == self.mock_security_manager
|
107
|
+
assert self.middleware.config == self.mock_security_manager.config
|
108
|
+
|
109
|
+
def test_call_success(self):
|
110
|
+
"""Test successful middleware call."""
|
111
|
+
environ = self.create_mock_environ()
|
112
|
+
mock_start_response = Mock()
|
113
|
+
|
114
|
+
# Mock successful authentication and authorization
|
115
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
116
|
+
|
117
|
+
# Mock successful authentication
|
118
|
+
auth_result = AuthResult(
|
119
|
+
is_valid=True,
|
120
|
+
status=AuthStatus.SUCCESS,
|
121
|
+
username="test_user",
|
122
|
+
roles=["user"],
|
123
|
+
auth_method=AuthMethod.API_KEY
|
124
|
+
)
|
125
|
+
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
126
|
+
|
127
|
+
self.middleware._validate_permissions = Mock(return_value=True)
|
128
|
+
|
129
|
+
# Mock successful response
|
130
|
+
mock_response = [b'{"message": "success"}']
|
131
|
+
self.middleware._process_request = Mock(return_value=mock_response)
|
132
|
+
|
133
|
+
result = self.middleware(environ, mock_start_response)
|
134
|
+
|
135
|
+
# Verify middleware processed successfully
|
136
|
+
assert result == mock_response
|
137
|
+
self.middleware._process_request.assert_called_once()
|
138
|
+
|
139
|
+
def test_call_rate_limit_exceeded(self):
|
140
|
+
"""Test middleware call with rate limit exceeded."""
|
141
|
+
environ = self.create_mock_environ()
|
142
|
+
mock_start_response = Mock()
|
143
|
+
|
144
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = False
|
145
|
+
|
146
|
+
result = self.middleware(environ, mock_start_response)
|
147
|
+
|
148
|
+
# Verify rate limit response
|
149
|
+
assert isinstance(result, list)
|
150
|
+
assert len(result) == 1
|
151
|
+
response_data = json.loads(result[0].decode('utf-8'))
|
152
|
+
assert response_data["error"] == "Rate limit exceeded"
|
153
|
+
|
154
|
+
# Verify start_response was called with correct status
|
155
|
+
mock_start_response.assert_called_once()
|
156
|
+
call_args = mock_start_response.call_args[0]
|
157
|
+
assert call_args[0] == '429 Too Many Requests'
|
158
|
+
|
159
|
+
def test_call_public_path(self):
|
160
|
+
"""Test middleware call with public path."""
|
161
|
+
environ = self.create_mock_environ(path="/health")
|
162
|
+
mock_start_response = Mock()
|
163
|
+
|
164
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
165
|
+
|
166
|
+
# Mock successful response for public path
|
167
|
+
mock_response = [b'{"status": "healthy"}']
|
168
|
+
self.middleware._process_request = Mock(return_value=mock_response)
|
169
|
+
|
170
|
+
result = self.middleware(environ, mock_start_response)
|
171
|
+
|
172
|
+
# Verify middleware processed successfully for public path
|
173
|
+
assert result == mock_response
|
174
|
+
self.middleware._process_request.assert_called_once()
|
175
|
+
|
176
|
+
def test_call_authentication_failed(self):
|
177
|
+
"""Test middleware call with authentication failure."""
|
178
|
+
environ = self.create_mock_environ()
|
179
|
+
mock_start_response = Mock()
|
180
|
+
|
181
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
182
|
+
|
183
|
+
# Mock failed authentication
|
184
|
+
auth_result = AuthResult(
|
185
|
+
is_valid=False,
|
186
|
+
status=AuthStatus.FAILED,
|
187
|
+
username=None,
|
188
|
+
roles=[],
|
189
|
+
auth_method=AuthMethod.API_KEY,
|
190
|
+
error_code=-32005,
|
191
|
+
error_message="Authentication failed"
|
192
|
+
)
|
193
|
+
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
194
|
+
|
195
|
+
result = self.middleware(environ, mock_start_response)
|
196
|
+
|
197
|
+
# Verify authentication error response
|
198
|
+
assert isinstance(result, list)
|
199
|
+
assert len(result) == 1
|
200
|
+
response_data = json.loads(result[0].decode('utf-8'))
|
201
|
+
assert response_data["error"] == "Authentication failed"
|
202
|
+
|
203
|
+
# Verify start_response was called with correct status
|
204
|
+
mock_start_response.assert_called_once()
|
205
|
+
call_args = mock_start_response.call_args[0]
|
206
|
+
assert call_args[0] == '401 Unauthorized'
|
207
|
+
|
208
|
+
def test_call_permission_denied(self):
|
209
|
+
"""Test middleware call with permission denied."""
|
210
|
+
environ = self.create_mock_environ()
|
211
|
+
mock_start_response = Mock()
|
212
|
+
|
213
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.return_value = True
|
214
|
+
|
215
|
+
# Mock successful authentication
|
216
|
+
auth_result = AuthResult(
|
217
|
+
is_valid=True,
|
218
|
+
status=AuthStatus.SUCCESS,
|
219
|
+
username="test_user",
|
220
|
+
roles=["user"],
|
221
|
+
auth_method=AuthMethod.API_KEY
|
222
|
+
)
|
223
|
+
self.middleware._authenticate_request = Mock(return_value=auth_result)
|
224
|
+
|
225
|
+
self.mock_security_manager.check_permissions.return_value = ValidationResult(
|
226
|
+
is_valid=False,
|
227
|
+
status=ValidationStatus.INVALID,
|
228
|
+
error_code=-32007,
|
229
|
+
error_message="Permission denied"
|
230
|
+
)
|
231
|
+
|
232
|
+
# Mock permission error response
|
233
|
+
mock_response = [b'{"error": "Permission denied"}']
|
234
|
+
self.middleware._process_request = Mock(return_value=mock_response)
|
235
|
+
|
236
|
+
result = self.middleware(environ, mock_start_response)
|
237
|
+
|
238
|
+
# Verify permission error response
|
239
|
+
assert result == mock_response
|
240
|
+
self.middleware._process_request.assert_called_once()
|
241
|
+
|
242
|
+
def test_get_rate_limit_identifier(self):
|
243
|
+
"""Test getting rate limit identifier from request."""
|
244
|
+
mock_request = self.create_mock_request()
|
245
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
246
|
+
assert result == "127.0.0.1"
|
247
|
+
|
248
|
+
def test_get_rate_limit_identifier_with_forwarded_for(self):
|
249
|
+
"""Test getting rate limit identifier with X-Forwarded-For header."""
|
250
|
+
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
251
|
+
mock_request = self.create_mock_request(headers=headers)
|
252
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
253
|
+
assert result == "192.168.1.1"
|
254
|
+
|
255
|
+
def test_get_rate_limit_identifier_with_real_ip(self):
|
256
|
+
"""Test getting rate limit identifier with X-Real-IP header."""
|
257
|
+
headers = {"X-Real-IP": "192.168.1.100"}
|
258
|
+
mock_request = self.create_mock_request(headers=headers)
|
259
|
+
result = self.middleware._get_rate_limit_identifier(mock_request)
|
260
|
+
assert result == "192.168.1.100"
|
261
|
+
|
262
|
+
def test_get_request_path(self):
|
263
|
+
"""Test getting request path from Flask request."""
|
264
|
+
mock_request = self.create_mock_request(path="/api/users")
|
265
|
+
result = self.middleware._get_request_path(mock_request)
|
266
|
+
assert result == "/api/users"
|
267
|
+
|
268
|
+
def test_get_required_permissions_from_request(self):
|
269
|
+
"""Test getting required permissions from request."""
|
270
|
+
mock_request = self.create_mock_request()
|
271
|
+
mock_request.required_permissions = ["read", "write"]
|
272
|
+
|
273
|
+
result = self.middleware._get_required_permissions(mock_request)
|
274
|
+
assert result == ["read", "write"]
|
275
|
+
|
276
|
+
def test_get_required_permissions_default(self):
|
277
|
+
"""Test getting required permissions when not set."""
|
278
|
+
mock_request = self.create_mock_request()
|
279
|
+
# Create a clean endpoint mock without any attributes
|
280
|
+
mock_endpoint = Mock()
|
281
|
+
mock_endpoint.required_permissions = None
|
282
|
+
mock_endpoint.required_roles = None
|
283
|
+
mock_endpoint.__permissions__ = None
|
284
|
+
mock_request.endpoint = mock_endpoint
|
285
|
+
result = self.middleware._get_required_permissions(mock_request)
|
286
|
+
assert result == []
|
287
|
+
|
288
|
+
def test_try_api_key_auth_success(self):
|
289
|
+
"""Test successful API key authentication."""
|
290
|
+
headers = {"X-API-Key": "valid_key"}
|
291
|
+
mock_request = self.create_mock_request(headers=headers)
|
292
|
+
|
293
|
+
expected_result = AuthResult(
|
294
|
+
is_valid=True,
|
295
|
+
status=AuthStatus.SUCCESS,
|
296
|
+
username="test_user",
|
297
|
+
roles=["user"],
|
298
|
+
auth_method=AuthMethod.API_KEY
|
299
|
+
)
|
300
|
+
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
301
|
+
|
302
|
+
result = self.middleware._try_api_key_auth(mock_request)
|
303
|
+
|
304
|
+
assert result == expected_result
|
305
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("valid_key")
|
306
|
+
|
307
|
+
def test_try_api_key_auth_from_authorization_header(self):
|
308
|
+
"""Test API key authentication from Authorization header."""
|
309
|
+
headers = {"Authorization": "Bearer api_key_123"}
|
310
|
+
mock_request = self.create_mock_request(headers=headers)
|
311
|
+
|
312
|
+
expected_result = AuthResult(
|
313
|
+
is_valid=True,
|
314
|
+
status=AuthStatus.SUCCESS,
|
315
|
+
username="test_user",
|
316
|
+
roles=["user"],
|
317
|
+
auth_method=AuthMethod.API_KEY
|
318
|
+
)
|
319
|
+
self.mock_auth_manager.authenticate_api_key.return_value = expected_result
|
320
|
+
|
321
|
+
result = self.middleware._try_api_key_auth(mock_request)
|
322
|
+
|
323
|
+
assert result == expected_result
|
324
|
+
self.mock_auth_manager.authenticate_api_key.assert_called_once_with("api_key_123")
|
325
|
+
|
326
|
+
def test_try_api_key_auth_no_key(self):
|
327
|
+
"""Test API key authentication with no key provided."""
|
328
|
+
mock_request = self.create_mock_request()
|
329
|
+
|
330
|
+
result = self.middleware._try_api_key_auth(mock_request)
|
331
|
+
|
332
|
+
assert result.is_valid is False
|
333
|
+
assert result.error_code == -32024
|
334
|
+
assert "API key not found" in result.error_message
|
335
|
+
|
336
|
+
def test_try_jwt_auth_success(self):
|
337
|
+
"""Test successful JWT authentication."""
|
338
|
+
headers = {"Authorization": "Bearer jwt_token_123"}
|
339
|
+
mock_request = self.create_mock_request(headers=headers)
|
340
|
+
|
341
|
+
expected_result = AuthResult(
|
342
|
+
is_valid=True,
|
343
|
+
status=AuthStatus.SUCCESS,
|
344
|
+
username="test_user",
|
345
|
+
roles=["user"],
|
346
|
+
auth_method=AuthMethod.JWT
|
347
|
+
)
|
348
|
+
self.mock_auth_manager.authenticate_jwt_token.return_value = expected_result
|
349
|
+
|
350
|
+
result = self.middleware._try_jwt_auth(mock_request)
|
351
|
+
|
352
|
+
assert result == expected_result
|
353
|
+
self.mock_auth_manager.authenticate_jwt_token.assert_called_once_with("jwt_token_123")
|
354
|
+
|
355
|
+
def test_try_jwt_auth_no_token(self):
|
356
|
+
"""Test JWT authentication with no token provided."""
|
357
|
+
mock_request = self.create_mock_request()
|
358
|
+
|
359
|
+
result = self.middleware._try_jwt_auth(mock_request)
|
360
|
+
|
361
|
+
assert result.is_valid is False
|
362
|
+
assert result.error_code == -32025
|
363
|
+
assert "JWT token not found" in result.error_message
|
364
|
+
|
365
|
+
def test_try_jwt_auth_invalid_header(self):
|
366
|
+
"""Test JWT authentication with invalid Authorization header."""
|
367
|
+
headers = {"Authorization": "Invalid jwt_token_123"}
|
368
|
+
mock_request = self.create_mock_request(headers=headers)
|
369
|
+
|
370
|
+
result = self.middleware._try_jwt_auth(mock_request)
|
371
|
+
|
372
|
+
assert result.is_valid is False
|
373
|
+
assert result.error_code == -32025
|
374
|
+
assert "JWT token not found" in result.error_message
|
375
|
+
|
376
|
+
def test_try_certificate_auth_not_implemented(self):
|
377
|
+
"""Test certificate authentication (not implemented)."""
|
378
|
+
mock_request = self.create_mock_request()
|
379
|
+
|
380
|
+
result = self.middleware._try_certificate_auth(mock_request)
|
381
|
+
|
382
|
+
assert result.is_valid is False
|
383
|
+
assert result.error_code == -32026
|
384
|
+
assert "not implemented" in result.error_message
|
385
|
+
|
386
|
+
def test_try_basic_auth_not_implemented(self):
|
387
|
+
"""Test basic authentication (not implemented)."""
|
388
|
+
mock_request = self.create_mock_request()
|
389
|
+
|
390
|
+
result = self.middleware._try_basic_auth(mock_request)
|
391
|
+
|
392
|
+
assert result.is_valid is False
|
393
|
+
assert result.error_code == -32027
|
394
|
+
assert "Basic authentication credentials not found" in result.error_message
|
395
|
+
|
396
|
+
def test_try_auth_method_unsupported(self):
|
397
|
+
"""Test authentication with unsupported method."""
|
398
|
+
mock_request = self.create_mock_request()
|
399
|
+
|
400
|
+
result = self.middleware._try_auth_method(mock_request, "unsupported_method")
|
401
|
+
|
402
|
+
assert result.is_valid is False
|
403
|
+
assert result.error_code == -32022
|
404
|
+
assert "Unsupported authentication method" in result.error_message
|
405
|
+
|
406
|
+
def test_apply_security_headers(self):
|
407
|
+
"""Test applying security headers to Flask response."""
|
408
|
+
mock_response = Mock(spec=Response)
|
409
|
+
mock_response.headers = {}
|
410
|
+
|
411
|
+
headers = {
|
412
|
+
"X-Content-Type-Options": "nosniff",
|
413
|
+
"X-Frame-Options": "DENY",
|
414
|
+
"X-XSS-Protection": "1; mode=block"
|
415
|
+
}
|
416
|
+
|
417
|
+
self.middleware._apply_security_headers(mock_response, headers)
|
418
|
+
|
419
|
+
assert mock_response.headers["X-Content-Type-Options"] == "nosniff"
|
420
|
+
assert mock_response.headers["X-Frame-Options"] == "DENY"
|
421
|
+
assert mock_response.headers["X-XSS-Protection"] == "1; mode=block"
|
422
|
+
|
423
|
+
def test_create_error_response(self):
|
424
|
+
"""Test creating error response."""
|
425
|
+
from flask import Flask
|
426
|
+
app = Flask(__name__)
|
427
|
+
|
428
|
+
with app.app_context():
|
429
|
+
result = self.middleware._create_error_response(400, "Bad request")
|
430
|
+
|
431
|
+
assert isinstance(result, Response)
|
432
|
+
assert result.status_code == 400
|
433
|
+
response_data = json.loads(result.get_data(as_text=True))
|
434
|
+
assert response_data["error"] == "Security violation"
|
435
|
+
assert response_data["message"] == "Bad request"
|
436
|
+
|
437
|
+
def test_rate_limit_response(self):
|
438
|
+
"""Test creating rate limit response."""
|
439
|
+
mock_start_response = Mock()
|
440
|
+
|
441
|
+
result = self.middleware._rate_limit_response(mock_start_response)
|
442
|
+
|
443
|
+
assert isinstance(result, list)
|
444
|
+
assert len(result) == 1
|
445
|
+
response_data = json.loads(result[0].decode('utf-8'))
|
446
|
+
assert response_data["error"] == "Rate limit exceeded"
|
447
|
+
|
448
|
+
# Verify start_response was called
|
449
|
+
mock_start_response.assert_called_once()
|
450
|
+
call_args = mock_start_response.call_args[0]
|
451
|
+
assert call_args[0] == '429 Too Many Requests'
|
452
|
+
|
453
|
+
# Check for Retry-After header
|
454
|
+
headers = call_args[1]
|
455
|
+
retry_after_header = next((h for h in headers if h[0] == 'Retry-After'), None)
|
456
|
+
assert retry_after_header is not None
|
457
|
+
assert retry_after_header[1] == '60'
|
458
|
+
|
459
|
+
def test_auth_error_response(self):
|
460
|
+
"""Test creating authentication error response."""
|
461
|
+
auth_result = AuthResult(
|
462
|
+
is_valid=False,
|
463
|
+
status=AuthStatus.FAILED,
|
464
|
+
username=None,
|
465
|
+
roles=[],
|
466
|
+
auth_method=AuthMethod.API_KEY,
|
467
|
+
error_code=-32005,
|
468
|
+
error_message="Invalid API key"
|
469
|
+
)
|
470
|
+
|
471
|
+
mock_start_response = Mock()
|
472
|
+
result = self.middleware._auth_error_response(auth_result, mock_start_response)
|
473
|
+
|
474
|
+
assert isinstance(result, list)
|
475
|
+
assert len(result) == 1
|
476
|
+
response_data = json.loads(result[0].decode('utf-8'))
|
477
|
+
assert response_data["error"] == "Authentication failed"
|
478
|
+
assert response_data["message"] == "Invalid API key"
|
479
|
+
|
480
|
+
# Verify start_response was called
|
481
|
+
mock_start_response.assert_called_once()
|
482
|
+
call_args = mock_start_response.call_args[0]
|
483
|
+
assert call_args[0] == '401 Unauthorized'
|
484
|
+
|
485
|
+
# Check for WWW-Authenticate header
|
486
|
+
headers = call_args[1]
|
487
|
+
www_auth_header = next((h for h in headers if h[0] == 'WWW-Authenticate'), None)
|
488
|
+
assert www_auth_header is not None
|
489
|
+
assert www_auth_header[1] == 'Bearer, ApiKey'
|
490
|
+
|
491
|
+
def test_permission_error_response(self):
|
492
|
+
"""Test creating permission error response."""
|
493
|
+
mock_start_response = Mock()
|
494
|
+
|
495
|
+
result = self.middleware._permission_error_response(mock_start_response)
|
496
|
+
|
497
|
+
assert isinstance(result, list)
|
498
|
+
assert len(result) == 1
|
499
|
+
response_data = json.loads(result[0].decode('utf-8'))
|
500
|
+
assert response_data["error"] == "Permission denied"
|
501
|
+
assert response_data["message"] == "Insufficient permissions to access this resource"
|
502
|
+
|
503
|
+
# Verify start_response was called
|
504
|
+
mock_start_response.assert_called_once()
|
505
|
+
call_args = mock_start_response.call_args[0]
|
506
|
+
assert call_args[0] == '403 Forbidden'
|
507
|
+
|
508
|
+
def test_get_client_ip_from_forwarded_for(self):
|
509
|
+
"""Test getting client IP from X-Forwarded-For header."""
|
510
|
+
headers = {"X-Forwarded-For": "192.168.1.1, 10.0.0.1"}
|
511
|
+
mock_request = self.create_mock_request(headers=headers)
|
512
|
+
|
513
|
+
result = self.middleware._get_client_ip(mock_request)
|
514
|
+
assert result == "192.168.1.1"
|
515
|
+
|
516
|
+
def test_get_client_ip_from_real_ip(self):
|
517
|
+
"""Test getting client IP from X-Real-IP header."""
|
518
|
+
headers = {"X-Real-IP": "192.168.1.100"}
|
519
|
+
mock_request = self.create_mock_request(headers=headers)
|
520
|
+
|
521
|
+
result = self.middleware._get_client_ip(mock_request)
|
522
|
+
assert result == "192.168.1.100"
|
523
|
+
|
524
|
+
def test_get_client_ip_from_remote_addr(self):
|
525
|
+
"""Test getting client IP from remote_addr."""
|
526
|
+
mock_request = self.create_mock_request()
|
527
|
+
mock_request.remote_addr = "192.168.1.50"
|
528
|
+
|
529
|
+
result = self.middleware._get_client_ip(mock_request)
|
530
|
+
assert result == "192.168.1.50"
|
531
|
+
|
532
|
+
def test_get_client_ip_fallback(self):
|
533
|
+
"""Test getting client IP with fallback."""
|
534
|
+
mock_request = self.create_mock_request()
|
535
|
+
mock_request.remote_addr = None
|
536
|
+
|
537
|
+
result = self.middleware._get_client_ip(mock_request)
|
538
|
+
assert result == "127.0.0.1"
|
539
|
+
|
540
|
+
def test_get_security_headers(self):
|
541
|
+
"""Test getting security headers."""
|
542
|
+
result = self.middleware._get_security_headers()
|
543
|
+
|
544
|
+
assert isinstance(result, list)
|
545
|
+
assert all(isinstance(header, tuple) and len(header) == 2 for header in result)
|
546
|
+
|
547
|
+
# Check for standard security headers
|
548
|
+
header_names = [header[0] for header in result]
|
549
|
+
assert 'X-Content-Type-Options' in header_names
|
550
|
+
assert 'X-Frame-Options' in header_names
|
551
|
+
assert 'X-XSS-Protection' in header_names
|
552
|
+
assert 'Strict-Transport-Security' in header_names
|
553
|
+
assert 'Content-Security-Policy' in header_names
|
554
|
+
assert 'Referrer-Policy' in header_names
|
555
|
+
|
556
|
+
def test_get_security_headers_with_custom_headers(self):
|
557
|
+
"""Test getting security headers with custom headers from config."""
|
558
|
+
self.mock_security_manager.config.auth.security_headers = {
|
559
|
+
"Custom-Security-Header": "custom_value"
|
560
|
+
}
|
561
|
+
|
562
|
+
result = self.middleware._get_security_headers()
|
563
|
+
|
564
|
+
header_names = [header[0] for header in result]
|
565
|
+
assert 'Custom-Security-Header' in header_names
|
566
|
+
|
567
|
+
custom_header = next(h for h in result if h[0] == 'Custom-Security-Header')
|
568
|
+
assert custom_header[1] == 'custom_value'
|
569
|
+
|
570
|
+
def test_call_with_exception(self):
|
571
|
+
"""Test middleware call with exception."""
|
572
|
+
environ = self.create_mock_environ()
|
573
|
+
mock_start_response = Mock()
|
574
|
+
|
575
|
+
# Mock an exception during processing
|
576
|
+
self.mock_security_manager.rate_limiter.check_rate_limit.side_effect = Exception("Test error")
|
577
|
+
|
578
|
+
with pytest.raises(FlaskMiddlewareError) as exc_info:
|
579
|
+
self.middleware(environ, mock_start_response)
|
580
|
+
|
581
|
+
assert "Middleware processing failed" in str(exc_info.value)
|
582
|
+
assert exc_info.value.error_code == -32003
|