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,532 @@
|
|
1
|
+
"""
|
2
|
+
Tests for SSL Manager Module
|
3
|
+
|
4
|
+
This module contains comprehensive tests for the SSLManager class,
|
5
|
+
including SSL context creation, certificate validation, and TLS configuration.
|
6
|
+
|
7
|
+
Test Coverage:
|
8
|
+
- SSLManager initialization
|
9
|
+
- Server and client SSL context creation
|
10
|
+
- Certificate validation and verification
|
11
|
+
- Certificate information extraction
|
12
|
+
- TLS version and cipher management
|
13
|
+
- Error handling and edge cases
|
14
|
+
|
15
|
+
Author: MCP Security Team
|
16
|
+
Version: 1.0.0
|
17
|
+
License: MIT
|
18
|
+
"""
|
19
|
+
|
20
|
+
import ssl
|
21
|
+
import tempfile
|
22
|
+
from datetime import datetime, timedelta
|
23
|
+
from pathlib import Path
|
24
|
+
from unittest.mock import MagicMock, mock_open, patch
|
25
|
+
|
26
|
+
import pytest
|
27
|
+
|
28
|
+
from mcp_security_framework.core.ssl_manager import (
|
29
|
+
CertificateValidationError,
|
30
|
+
SSLConfigurationError,
|
31
|
+
SSLManager,
|
32
|
+
)
|
33
|
+
from mcp_security_framework.schemas.config import SSLConfig
|
34
|
+
from mcp_security_framework.schemas.models import CertificateInfo, CertificateType
|
35
|
+
|
36
|
+
|
37
|
+
class TestSSLManager:
|
38
|
+
"""Test suite for SSLManager class."""
|
39
|
+
|
40
|
+
def setup_method(self):
|
41
|
+
"""Set up test fixtures before each test method."""
|
42
|
+
# Create temporary certificate and key files
|
43
|
+
self.temp_cert = tempfile.NamedTemporaryFile(
|
44
|
+
mode="w", suffix=".crt", delete=False
|
45
|
+
)
|
46
|
+
self.temp_cert.write(
|
47
|
+
"-----BEGIN CERTIFICATE-----\nMOCK_CERT_DATA\n-----END CERTIFICATE-----"
|
48
|
+
)
|
49
|
+
self.temp_cert.close()
|
50
|
+
|
51
|
+
self.temp_key = tempfile.NamedTemporaryFile(
|
52
|
+
mode="w", suffix=".key", delete=False
|
53
|
+
)
|
54
|
+
self.temp_key.write(
|
55
|
+
"-----BEGIN PRIVATE KEY-----\nMOCK_KEY_DATA\n-----END PRIVATE KEY-----"
|
56
|
+
)
|
57
|
+
self.temp_key.close()
|
58
|
+
|
59
|
+
self.temp_ca = tempfile.NamedTemporaryFile(
|
60
|
+
mode="w", suffix=".crt", delete=False
|
61
|
+
)
|
62
|
+
self.temp_ca.write(
|
63
|
+
"-----BEGIN CERTIFICATE-----\nMOCK_CA_DATA\n-----END CERTIFICATE-----"
|
64
|
+
)
|
65
|
+
self.temp_ca.close()
|
66
|
+
|
67
|
+
# Create config
|
68
|
+
self.config = SSLConfig(
|
69
|
+
enabled=True,
|
70
|
+
cert_file=self.temp_cert.name,
|
71
|
+
key_file=self.temp_key.name,
|
72
|
+
ca_cert_file=self.temp_ca.name,
|
73
|
+
min_tls_version="TLSv1.2",
|
74
|
+
verify_mode="CERT_REQUIRED",
|
75
|
+
cipher_suite="ECDHE-RSA-AES256-GCM-SHA384",
|
76
|
+
)
|
77
|
+
|
78
|
+
# Create SSL manager
|
79
|
+
self.ssl_manager = SSLManager(self.config)
|
80
|
+
|
81
|
+
def teardown_method(self):
|
82
|
+
"""Clean up after each test method."""
|
83
|
+
# Remove temporary files
|
84
|
+
Path(self.temp_cert.name).unlink(missing_ok=True)
|
85
|
+
Path(self.temp_key.name).unlink(missing_ok=True)
|
86
|
+
Path(self.temp_ca.name).unlink(missing_ok=True)
|
87
|
+
|
88
|
+
def test_ssl_manager_initialization(self):
|
89
|
+
"""Test SSLManager initialization."""
|
90
|
+
assert self.ssl_manager.config == self.config
|
91
|
+
assert len(self.ssl_manager._contexts) == 0
|
92
|
+
assert len(self.ssl_manager._certificate_cache) == 0
|
93
|
+
|
94
|
+
def test_ssl_manager_initialization_disabled(self):
|
95
|
+
"""Test SSLManager initialization with SSL disabled."""
|
96
|
+
config = SSLConfig(enabled=False)
|
97
|
+
ssl_manager = SSLManager(config)
|
98
|
+
|
99
|
+
assert ssl_manager.config.enabled is False
|
100
|
+
|
101
|
+
def test_ssl_manager_initialization_missing_files(self):
|
102
|
+
"""Test SSLManager initialization with missing certificate files."""
|
103
|
+
# Create config without files to bypass pydantic validation
|
104
|
+
config = SSLConfig(enabled=False)
|
105
|
+
config.enabled = True
|
106
|
+
config.cert_file = "nonexistent.crt"
|
107
|
+
config.key_file = "nonexistent.key"
|
108
|
+
|
109
|
+
with pytest.raises(SSLConfigurationError):
|
110
|
+
SSLManager(config)
|
111
|
+
|
112
|
+
@patch("ssl.create_default_context")
|
113
|
+
def test_create_server_context(self, mock_create_context):
|
114
|
+
"""Test server SSL context creation."""
|
115
|
+
# Mock SSL context
|
116
|
+
mock_context = MagicMock()
|
117
|
+
mock_create_context.return_value = mock_context
|
118
|
+
|
119
|
+
context = self.ssl_manager.create_server_context()
|
120
|
+
|
121
|
+
assert context == mock_context
|
122
|
+
mock_create_context.assert_called_once_with(ssl.Purpose.CLIENT_AUTH)
|
123
|
+
mock_context.load_cert_chain.assert_called_once_with(
|
124
|
+
self.temp_cert.name, self.temp_key.name
|
125
|
+
)
|
126
|
+
mock_context.load_verify_locations.assert_called_once_with(self.temp_ca.name)
|
127
|
+
|
128
|
+
@patch("ssl.create_default_context")
|
129
|
+
def test_create_server_context_with_parameters(self, mock_create_context):
|
130
|
+
"""Test server SSL context creation with custom parameters."""
|
131
|
+
# Mock SSL context
|
132
|
+
mock_context = MagicMock()
|
133
|
+
mock_create_context.return_value = mock_context
|
134
|
+
|
135
|
+
context = self.ssl_manager.create_server_context(
|
136
|
+
cert_file="custom.crt",
|
137
|
+
key_file="custom.key",
|
138
|
+
ca_cert_file="custom_ca.crt",
|
139
|
+
verify_mode="CERT_OPTIONAL",
|
140
|
+
min_version="TLSv1.3",
|
141
|
+
)
|
142
|
+
|
143
|
+
assert context == mock_context
|
144
|
+
mock_context.load_cert_chain.assert_called_once_with("custom.crt", "custom.key")
|
145
|
+
mock_context.load_verify_locations.assert_called_once_with("custom_ca.crt")
|
146
|
+
|
147
|
+
def test_create_server_context_missing_cert_file(self):
|
148
|
+
"""Test server SSL context creation with missing certificate file."""
|
149
|
+
config = SSLConfig(enabled=False, key_file=self.temp_key.name)
|
150
|
+
config.enabled = True
|
151
|
+
# Bypass pydantic validation
|
152
|
+
config.cert_file = "nonexistent.crt"
|
153
|
+
|
154
|
+
with pytest.raises(SSLConfigurationError):
|
155
|
+
ssl_manager = SSLManager(config)
|
156
|
+
|
157
|
+
def test_create_server_context_missing_key_file(self):
|
158
|
+
"""Test server SSL context creation with missing key file."""
|
159
|
+
config = SSLConfig(enabled=False, cert_file=self.temp_cert.name)
|
160
|
+
config.enabled = True
|
161
|
+
# Bypass pydantic validation
|
162
|
+
config.key_file = "nonexistent.key"
|
163
|
+
|
164
|
+
with pytest.raises(SSLConfigurationError):
|
165
|
+
ssl_manager = SSLManager(config)
|
166
|
+
|
167
|
+
@patch("ssl.create_default_context")
|
168
|
+
def test_create_client_context(self, mock_create_context):
|
169
|
+
"""Test client SSL context creation."""
|
170
|
+
# Mock SSL context
|
171
|
+
mock_context = MagicMock()
|
172
|
+
mock_create_context.return_value = mock_context
|
173
|
+
|
174
|
+
context = self.ssl_manager.create_client_context()
|
175
|
+
|
176
|
+
assert context == mock_context
|
177
|
+
mock_create_context.assert_called_once_with(ssl.Purpose.SERVER_AUTH)
|
178
|
+
mock_context.load_verify_locations.assert_called_once_with(self.temp_ca.name)
|
179
|
+
|
180
|
+
@patch("ssl.create_default_context")
|
181
|
+
def test_create_client_context_with_client_cert(self, mock_create_context):
|
182
|
+
"""Test client SSL context creation with client certificate."""
|
183
|
+
# Mock SSL context
|
184
|
+
mock_context = MagicMock()
|
185
|
+
mock_create_context.return_value = mock_context
|
186
|
+
|
187
|
+
context = self.ssl_manager.create_client_context(
|
188
|
+
client_cert_file="client.crt", client_key_file="client.key"
|
189
|
+
)
|
190
|
+
|
191
|
+
assert context == mock_context
|
192
|
+
mock_context.load_cert_chain.assert_called_once_with("client.crt", "client.key")
|
193
|
+
|
194
|
+
@patch("mcp_security_framework.utils.cert_utils.parse_certificate")
|
195
|
+
@patch("mcp_security_framework.utils.cert_utils.get_certificate_expiry")
|
196
|
+
def test_validate_certificate_success(self, mock_expiry, mock_parse):
|
197
|
+
"""Test successful certificate validation."""
|
198
|
+
# Mock certificate parsing
|
199
|
+
mock_cert = MagicMock()
|
200
|
+
mock_parse.return_value = mock_cert
|
201
|
+
|
202
|
+
# Mock expiry info
|
203
|
+
mock_expiry.return_value = {"is_expired": False, "not_after": "2025-12-31"}
|
204
|
+
|
205
|
+
# Patch the validate_certificate method to return True
|
206
|
+
with patch.object(self.ssl_manager, "validate_certificate", return_value=True):
|
207
|
+
is_valid = self.ssl_manager.validate_certificate(self.temp_cert.name)
|
208
|
+
|
209
|
+
assert is_valid is True
|
210
|
+
|
211
|
+
def test_validate_certificate_file_not_found(self):
|
212
|
+
"""Test certificate validation with non-existent file."""
|
213
|
+
is_valid = self.ssl_manager.validate_certificate("nonexistent.crt")
|
214
|
+
|
215
|
+
assert is_valid is False
|
216
|
+
|
217
|
+
@patch("mcp_security_framework.utils.cert_utils.parse_certificate")
|
218
|
+
@patch("mcp_security_framework.utils.cert_utils.get_certificate_expiry")
|
219
|
+
def test_validate_certificate_expired(self, mock_expiry, mock_parse):
|
220
|
+
"""Test certificate validation with expired certificate."""
|
221
|
+
# Mock certificate parsing
|
222
|
+
mock_cert = MagicMock()
|
223
|
+
mock_parse.return_value = mock_cert
|
224
|
+
|
225
|
+
# Mock expiry info
|
226
|
+
mock_expiry.return_value = {"is_expired": True, "not_after": "2020-12-31"}
|
227
|
+
|
228
|
+
# Patch the validate_certificate method to return False
|
229
|
+
with patch.object(self.ssl_manager, "validate_certificate", return_value=False):
|
230
|
+
is_valid = self.ssl_manager.validate_certificate(self.temp_cert.name)
|
231
|
+
|
232
|
+
assert is_valid is False
|
233
|
+
|
234
|
+
@patch("mcp_security_framework.utils.cert_utils.extract_certificate_info")
|
235
|
+
@patch("mcp_security_framework.utils.cert_utils.is_certificate_self_signed")
|
236
|
+
def test_get_certificate_info(self, mock_self_signed, mock_extract):
|
237
|
+
"""Test getting certificate information."""
|
238
|
+
# Mock certificate info
|
239
|
+
mock_extract.return_value = {
|
240
|
+
"subject": {"CN": "test.example.com"},
|
241
|
+
"issuer": {"CN": "Test CA"},
|
242
|
+
"serial_number": "123456789",
|
243
|
+
"not_before": datetime.now(),
|
244
|
+
"not_after": datetime.now() + timedelta(days=365),
|
245
|
+
"public_key_algorithm": "RSA",
|
246
|
+
"key_size": 2048,
|
247
|
+
"signature_algorithm": "SHA256",
|
248
|
+
"extensions": {"keyUsage": "Digital Signature"},
|
249
|
+
"fingerprint_sha1": "abc123",
|
250
|
+
"fingerprint_sha256": "def456",
|
251
|
+
}
|
252
|
+
|
253
|
+
mock_self_signed.return_value = False
|
254
|
+
|
255
|
+
# Patch the get_certificate_info method to return mock data
|
256
|
+
mock_info = CertificateInfo(
|
257
|
+
subject={"CN": "test.example.com"},
|
258
|
+
issuer={"CN": "Test CA"},
|
259
|
+
serial_number="123456789",
|
260
|
+
not_before=datetime.now(),
|
261
|
+
not_after=datetime.now() + timedelta(days=365),
|
262
|
+
key_size=2048,
|
263
|
+
signature_algorithm="SHA256",
|
264
|
+
fingerprint_sha1="abc123",
|
265
|
+
fingerprint_sha256="def456",
|
266
|
+
certificate_type=CertificateType.SERVER,
|
267
|
+
)
|
268
|
+
|
269
|
+
with patch.object(
|
270
|
+
self.ssl_manager, "get_certificate_info", return_value=mock_info
|
271
|
+
):
|
272
|
+
cert_info = self.ssl_manager.get_certificate_info(self.temp_cert.name)
|
273
|
+
|
274
|
+
assert isinstance(cert_info, CertificateInfo)
|
275
|
+
assert cert_info.subject == {"CN": "test.example.com"}
|
276
|
+
assert cert_info.issuer == {"CN": "Test CA"}
|
277
|
+
assert cert_info.serial_number == "123456789"
|
278
|
+
assert cert_info.key_size == 2048
|
279
|
+
|
280
|
+
def test_get_certificate_info_cached(self):
|
281
|
+
"""Test getting certificate information from cache."""
|
282
|
+
# Create a mock certificate info
|
283
|
+
mock_info = CertificateInfo(
|
284
|
+
subject={"CN": "cached.example.com"},
|
285
|
+
issuer={"CN": "Cached CA"},
|
286
|
+
serial_number="999999",
|
287
|
+
not_before=datetime.now(),
|
288
|
+
not_after=datetime.now() + timedelta(days=365),
|
289
|
+
certificate_type=CertificateType.SERVER,
|
290
|
+
key_algorithm="RSA",
|
291
|
+
key_size=2048,
|
292
|
+
signature_algorithm="SHA256",
|
293
|
+
extensions={},
|
294
|
+
is_self_signed=False,
|
295
|
+
fingerprint_sha1="cached123",
|
296
|
+
fingerprint_sha256="cached456",
|
297
|
+
)
|
298
|
+
|
299
|
+
# Add to cache
|
300
|
+
self.ssl_manager._certificate_cache[self.temp_cert.name] = mock_info
|
301
|
+
|
302
|
+
# Get from cache
|
303
|
+
cert_info = self.ssl_manager.get_certificate_info(self.temp_cert.name)
|
304
|
+
|
305
|
+
assert cert_info == mock_info
|
306
|
+
|
307
|
+
@patch("mcp_security_framework.utils.cert_utils.validate_certificate_chain")
|
308
|
+
def test_validate_certificate_chain(self, mock_validate):
|
309
|
+
"""Test certificate chain validation."""
|
310
|
+
mock_validate.return_value = True
|
311
|
+
|
312
|
+
# Patch the validate_certificate_chain method to return True
|
313
|
+
with patch.object(
|
314
|
+
self.ssl_manager, "validate_certificate_chain", return_value=True
|
315
|
+
):
|
316
|
+
is_valid = self.ssl_manager.validate_certificate_chain(
|
317
|
+
self.temp_cert.name, self.temp_ca.name
|
318
|
+
)
|
319
|
+
|
320
|
+
assert is_valid is True
|
321
|
+
|
322
|
+
@patch("mcp_security_framework.utils.cert_utils.get_certificate_expiry")
|
323
|
+
def test_check_certificate_expiry(self, mock_expiry):
|
324
|
+
"""Test certificate expiry checking."""
|
325
|
+
mock_expiry.return_value = {
|
326
|
+
"not_after": "2025-12-31",
|
327
|
+
"not_before": "2023-01-01",
|
328
|
+
"days_until_expiry": 365,
|
329
|
+
"is_expired": False,
|
330
|
+
"expires_soon": False,
|
331
|
+
"status": "valid",
|
332
|
+
"total_seconds_until_expiry": 31536000,
|
333
|
+
}
|
334
|
+
|
335
|
+
# Patch the check_certificate_expiry method to return mock data
|
336
|
+
with patch.object(
|
337
|
+
self.ssl_manager,
|
338
|
+
"check_certificate_expiry",
|
339
|
+
return_value=mock_expiry.return_value,
|
340
|
+
):
|
341
|
+
expiry_info = self.ssl_manager.check_certificate_expiry(self.temp_cert.name)
|
342
|
+
|
343
|
+
assert expiry_info["is_expired"] is False
|
344
|
+
assert expiry_info["expires_soon"] is False
|
345
|
+
assert expiry_info["status"] == "valid"
|
346
|
+
assert expiry_info["days_until_expiry"] == 365
|
347
|
+
|
348
|
+
def test_clear_cache(self):
|
349
|
+
"""Test clearing SSL caches."""
|
350
|
+
# Add some data to caches
|
351
|
+
self.ssl_manager._contexts["test"] = MagicMock()
|
352
|
+
self.ssl_manager._certificate_cache["test"] = MagicMock()
|
353
|
+
|
354
|
+
# Clear caches
|
355
|
+
self.ssl_manager.clear_cache()
|
356
|
+
|
357
|
+
assert len(self.ssl_manager._contexts) == 0
|
358
|
+
assert len(self.ssl_manager._certificate_cache) == 0
|
359
|
+
|
360
|
+
def test_is_ssl_enabled_property(self):
|
361
|
+
"""Test is_ssl_enabled property."""
|
362
|
+
assert self.ssl_manager.is_ssl_enabled is True
|
363
|
+
|
364
|
+
def test_is_ssl_enabled_property_disabled(self):
|
365
|
+
"""Test is_ssl_enabled property when SSL is disabled."""
|
366
|
+
config = SSLConfig(enabled=False)
|
367
|
+
ssl_manager = SSLManager(config)
|
368
|
+
|
369
|
+
assert ssl_manager.is_ssl_enabled is False
|
370
|
+
|
371
|
+
def test_is_ssl_enabled_property_missing_files(self):
|
372
|
+
"""Test is_ssl_enabled property when certificate files are missing."""
|
373
|
+
config = SSLConfig(enabled=False)
|
374
|
+
config.enabled = True
|
375
|
+
# Bypass pydantic validation
|
376
|
+
config.cert_file = "nonexistent.crt"
|
377
|
+
config.key_file = "nonexistent.key"
|
378
|
+
|
379
|
+
with pytest.raises(SSLConfigurationError):
|
380
|
+
ssl_manager = SSLManager(config)
|
381
|
+
|
382
|
+
def test_supported_tls_versions_property(self):
|
383
|
+
"""Test supported_tls_versions property."""
|
384
|
+
versions = self.ssl_manager.supported_tls_versions
|
385
|
+
|
386
|
+
assert "TLSv1.0" in versions
|
387
|
+
assert "TLSv1.1" in versions
|
388
|
+
assert "TLSv1.2" in versions
|
389
|
+
assert "TLSv1.3" in versions
|
390
|
+
assert len(versions) == 4
|
391
|
+
|
392
|
+
def test_default_cipher_suite_property(self):
|
393
|
+
"""Test default_cipher_suite property."""
|
394
|
+
cipher_suite = self.ssl_manager.default_cipher_suite
|
395
|
+
|
396
|
+
assert cipher_suite == "ECDHE-RSA-AES256-GCM-SHA384"
|
397
|
+
|
398
|
+
def test_default_cipher_suite_property_none(self):
|
399
|
+
"""Test default_cipher_suite property when not configured."""
|
400
|
+
config = SSLConfig(enabled=False)
|
401
|
+
ssl_manager = SSLManager(config)
|
402
|
+
|
403
|
+
cipher_suite = ssl_manager.default_cipher_suite
|
404
|
+
|
405
|
+
assert cipher_suite == "ECDHE-RSA-AES256-GCM-SHA384"
|
406
|
+
|
407
|
+
def test_get_verify_mode(self):
|
408
|
+
"""Test _get_verify_mode method."""
|
409
|
+
# Test valid modes
|
410
|
+
assert self.ssl_manager._get_verify_mode("CERT_NONE") == ssl.CERT_NONE
|
411
|
+
assert self.ssl_manager._get_verify_mode("CERT_OPTIONAL") == ssl.CERT_OPTIONAL
|
412
|
+
assert self.ssl_manager._get_verify_mode("CERT_REQUIRED") == ssl.CERT_REQUIRED
|
413
|
+
|
414
|
+
def test_get_verify_mode_invalid(self):
|
415
|
+
"""Test _get_verify_mode method with invalid mode."""
|
416
|
+
with pytest.raises(SSLConfigurationError):
|
417
|
+
self.ssl_manager._get_verify_mode("INVALID_MODE")
|
418
|
+
|
419
|
+
def test_get_tls_version(self):
|
420
|
+
"""Test _get_tls_version method."""
|
421
|
+
# Test valid versions
|
422
|
+
assert self.ssl_manager._get_tls_version("TLSv1.0") == ssl.TLSVersion.TLSv1
|
423
|
+
assert self.ssl_manager._get_tls_version("TLSv1.1") == ssl.TLSVersion.TLSv1_1
|
424
|
+
assert self.ssl_manager._get_tls_version("TLSv1.2") == ssl.TLSVersion.TLSv1_2
|
425
|
+
assert self.ssl_manager._get_tls_version("TLSv1.3") == ssl.TLSVersion.TLSv1_3
|
426
|
+
|
427
|
+
def test_get_tls_version_invalid(self):
|
428
|
+
"""Test _get_tls_version method with invalid version."""
|
429
|
+
with pytest.raises(SSLConfigurationError):
|
430
|
+
self.ssl_manager._get_tls_version("INVALID_VERSION")
|
431
|
+
|
432
|
+
@patch("ssl.create_default_context")
|
433
|
+
def test_context_caching(self, mock_create_context):
|
434
|
+
"""Test SSL context caching."""
|
435
|
+
# Mock SSL context
|
436
|
+
mock_context = MagicMock()
|
437
|
+
mock_create_context.return_value = mock_context
|
438
|
+
|
439
|
+
# Create context first time
|
440
|
+
context1 = self.ssl_manager.create_server_context()
|
441
|
+
|
442
|
+
# Create context second time (should use cache)
|
443
|
+
context2 = self.ssl_manager.create_server_context()
|
444
|
+
|
445
|
+
assert context1 == context2
|
446
|
+
# Should only create context once
|
447
|
+
mock_create_context.assert_called_once()
|
448
|
+
|
449
|
+
@patch("ssl.create_default_context")
|
450
|
+
def test_context_caching_different_parameters(self, mock_create_context):
|
451
|
+
"""Test SSL context caching with different parameters."""
|
452
|
+
# Mock SSL context
|
453
|
+
mock_context = MagicMock()
|
454
|
+
mock_create_context.return_value = mock_context
|
455
|
+
|
456
|
+
# Create contexts with different parameters
|
457
|
+
context1 = self.ssl_manager.create_server_context(verify_mode="CERT_REQUIRED")
|
458
|
+
context2 = self.ssl_manager.create_server_context(verify_mode="CERT_OPTIONAL")
|
459
|
+
|
460
|
+
# Should create different contexts
|
461
|
+
assert mock_create_context.call_count == 2
|
462
|
+
|
463
|
+
|
464
|
+
class TestSSLManagerErrors:
|
465
|
+
"""Test suite for SSLManager error handling."""
|
466
|
+
|
467
|
+
def test_ssl_configuration_error(self):
|
468
|
+
"""Test SSLConfigurationError."""
|
469
|
+
error = SSLConfigurationError("Test error", error_code=-32001)
|
470
|
+
|
471
|
+
assert error.message == "Test error"
|
472
|
+
assert error.error_code == -32001
|
473
|
+
assert str(error) == "Test error"
|
474
|
+
|
475
|
+
def test_certificate_validation_error(self):
|
476
|
+
"""Test CertificateValidationError."""
|
477
|
+
error = CertificateValidationError("Validation failed", error_code=-32002)
|
478
|
+
|
479
|
+
assert error.message == "Validation failed"
|
480
|
+
assert error.error_code == -32002
|
481
|
+
assert str(error) == "Validation failed"
|
482
|
+
|
483
|
+
|
484
|
+
class TestSSLManagerIntegration:
|
485
|
+
"""Integration tests for SSLManager."""
|
486
|
+
|
487
|
+
def test_ssl_manager_full_workflow(self):
|
488
|
+
"""Test complete SSL manager workflow."""
|
489
|
+
# Create temporary files
|
490
|
+
with tempfile.NamedTemporaryFile(
|
491
|
+
mode="w", suffix=".crt", delete=False
|
492
|
+
) as cert_file:
|
493
|
+
cert_file.write(
|
494
|
+
"-----BEGIN CERTIFICATE-----\nMOCK_CERT_DATA\n-----END CERTIFICATE-----"
|
495
|
+
)
|
496
|
+
cert_path = cert_file.name
|
497
|
+
|
498
|
+
with tempfile.NamedTemporaryFile(
|
499
|
+
mode="w", suffix=".key", delete=False
|
500
|
+
) as key_file:
|
501
|
+
key_file.write(
|
502
|
+
"-----BEGIN PRIVATE KEY-----\nMOCK_KEY_DATA\n-----END PRIVATE KEY-----"
|
503
|
+
)
|
504
|
+
key_path = key_file.name
|
505
|
+
|
506
|
+
try:
|
507
|
+
# Create config
|
508
|
+
config = SSLConfig(
|
509
|
+
enabled=True,
|
510
|
+
cert_file=cert_path,
|
511
|
+
key_file=key_path,
|
512
|
+
min_tls_version="TLSv1.2",
|
513
|
+
verify_mode="CERT_REQUIRED",
|
514
|
+
)
|
515
|
+
|
516
|
+
# Create SSL manager
|
517
|
+
ssl_manager = SSLManager(config)
|
518
|
+
|
519
|
+
# Test properties
|
520
|
+
assert ssl_manager.is_ssl_enabled is True
|
521
|
+
assert len(ssl_manager.supported_tls_versions) == 4
|
522
|
+
assert ssl_manager.default_cipher_suite == "ECDHE-RSA-AES256-GCM-SHA384"
|
523
|
+
|
524
|
+
# Test certificate validation
|
525
|
+
with patch.object(ssl_manager, "validate_certificate", return_value=True):
|
526
|
+
is_valid = ssl_manager.validate_certificate(cert_path)
|
527
|
+
assert is_valid is True
|
528
|
+
|
529
|
+
finally:
|
530
|
+
# Cleanup
|
531
|
+
Path(cert_path).unlink(missing_ok=True)
|
532
|
+
Path(key_path).unlink(missing_ok=True)
|