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,879 @@
|
|
1
|
+
"""
|
2
|
+
Data Models Test Module
|
3
|
+
|
4
|
+
This module provides comprehensive unit tests for all data models
|
5
|
+
in the MCP Security Framework. It tests validation, properties,
|
6
|
+
and edge cases for all model classes.
|
7
|
+
|
8
|
+
Test Classes:
|
9
|
+
TestAuthResult: Tests for authentication result model
|
10
|
+
TestValidationResult: Tests for validation result model
|
11
|
+
TestCertificateInfo: Tests for certificate information model
|
12
|
+
TestCertificatePair: Tests for certificate pair model
|
13
|
+
TestRateLimitStatus: Tests for rate limiting status model
|
14
|
+
TestUserCredentials: Tests for user credentials model
|
15
|
+
TestRolePermissions: Tests for role permissions model
|
16
|
+
TestCertificateChain: Tests for certificate chain model
|
17
|
+
|
18
|
+
Author: MCP Security Team
|
19
|
+
Version: 1.0.0
|
20
|
+
License: MIT
|
21
|
+
"""
|
22
|
+
|
23
|
+
from datetime import datetime, timedelta, timezone
|
24
|
+
|
25
|
+
import pytest
|
26
|
+
from pydantic import ValidationError
|
27
|
+
|
28
|
+
from mcp_security_framework.schemas.models import (
|
29
|
+
AuthMethod,
|
30
|
+
AuthResult,
|
31
|
+
AuthStatus,
|
32
|
+
CertificateChain,
|
33
|
+
CertificateInfo,
|
34
|
+
CertificatePair,
|
35
|
+
CertificateType,
|
36
|
+
RateLimitStatus,
|
37
|
+
RolePermissions,
|
38
|
+
UserCredentials,
|
39
|
+
ValidationResult,
|
40
|
+
ValidationStatus,
|
41
|
+
)
|
42
|
+
|
43
|
+
|
44
|
+
class TestAuthResult:
|
45
|
+
"""Test suite for AuthResult class."""
|
46
|
+
|
47
|
+
def test_auth_result_success(self):
|
48
|
+
"""Test AuthResult with successful authentication."""
|
49
|
+
auth_result = AuthResult(
|
50
|
+
is_valid=True,
|
51
|
+
status=AuthStatus.SUCCESS,
|
52
|
+
username="testuser",
|
53
|
+
user_id="12345",
|
54
|
+
roles=["user", "admin"],
|
55
|
+
permissions={"read", "write", "delete"},
|
56
|
+
auth_method=AuthMethod.API_KEY,
|
57
|
+
token_expiry=datetime.now(timezone.utc) + timedelta(hours=1),
|
58
|
+
)
|
59
|
+
|
60
|
+
assert auth_result.is_valid is True
|
61
|
+
assert auth_result.status == AuthStatus.SUCCESS
|
62
|
+
assert auth_result.username == "testuser"
|
63
|
+
assert auth_result.user_id == "12345"
|
64
|
+
assert auth_result.roles == ["user", "admin"]
|
65
|
+
assert auth_result.permissions == {"read", "write", "delete"}
|
66
|
+
assert auth_result.auth_method == AuthMethod.API_KEY
|
67
|
+
assert auth_result.error_code is None
|
68
|
+
assert auth_result.error_message is None
|
69
|
+
|
70
|
+
def test_auth_result_failure(self):
|
71
|
+
"""Test AuthResult with failed authentication."""
|
72
|
+
auth_result = AuthResult(
|
73
|
+
is_valid=False,
|
74
|
+
status=AuthStatus.FAILED,
|
75
|
+
error_code=401,
|
76
|
+
error_message="Invalid credentials",
|
77
|
+
)
|
78
|
+
|
79
|
+
assert auth_result.is_valid is False
|
80
|
+
assert auth_result.status == AuthStatus.FAILED
|
81
|
+
assert auth_result.error_code == 401
|
82
|
+
assert auth_result.error_message == "Invalid credentials"
|
83
|
+
assert auth_result.username is None
|
84
|
+
assert auth_result.user_id is None
|
85
|
+
|
86
|
+
def test_auth_result_invalid_username(self):
|
87
|
+
"""Test AuthResult with invalid username."""
|
88
|
+
with pytest.raises(ValidationError) as exc_info:
|
89
|
+
AuthResult(
|
90
|
+
is_valid=True,
|
91
|
+
status=AuthStatus.SUCCESS,
|
92
|
+
username=" ", # Empty after strip
|
93
|
+
)
|
94
|
+
|
95
|
+
assert "Username cannot be empty" in str(exc_info.value)
|
96
|
+
|
97
|
+
def test_auth_result_validation_consistency_success_with_error(self):
|
98
|
+
"""Test AuthResult validation when success has error code."""
|
99
|
+
with pytest.raises(ValidationError) as exc_info:
|
100
|
+
AuthResult(is_valid=True, status=AuthStatus.SUCCESS, error_code=401)
|
101
|
+
|
102
|
+
assert "Valid authentication cannot have error code" in str(exc_info.value)
|
103
|
+
|
104
|
+
def test_auth_result_validation_consistency_failure_without_error(self):
|
105
|
+
"""Test AuthResult validation when failure has no error."""
|
106
|
+
with pytest.raises(ValidationError) as exc_info:
|
107
|
+
AuthResult(is_valid=False, status=AuthStatus.SUCCESS)
|
108
|
+
|
109
|
+
assert "Invalid authentication cannot have SUCCESS status" in str(
|
110
|
+
exc_info.value
|
111
|
+
)
|
112
|
+
|
113
|
+
def test_auth_result_properties(self):
|
114
|
+
"""Test AuthResult properties."""
|
115
|
+
# Not expired
|
116
|
+
auth_result = AuthResult(
|
117
|
+
is_valid=True,
|
118
|
+
status=AuthStatus.SUCCESS,
|
119
|
+
token_expiry=datetime.now(timezone.utc) + timedelta(hours=2),
|
120
|
+
)
|
121
|
+
|
122
|
+
assert auth_result.is_expired is False
|
123
|
+
assert auth_result.expires_soon is False
|
124
|
+
|
125
|
+
# Expires soon
|
126
|
+
auth_result = AuthResult(
|
127
|
+
is_valid=True,
|
128
|
+
status=AuthStatus.SUCCESS,
|
129
|
+
token_expiry=datetime.now(timezone.utc) + timedelta(minutes=30),
|
130
|
+
)
|
131
|
+
|
132
|
+
assert auth_result.is_expired is False
|
133
|
+
assert auth_result.expires_soon is True
|
134
|
+
|
135
|
+
# Expired
|
136
|
+
auth_result = AuthResult(
|
137
|
+
is_valid=True,
|
138
|
+
status=AuthStatus.SUCCESS,
|
139
|
+
token_expiry=datetime.now(timezone.utc) - timedelta(hours=1),
|
140
|
+
)
|
141
|
+
|
142
|
+
assert auth_result.is_expired is True
|
143
|
+
assert auth_result.expires_soon is True
|
144
|
+
|
145
|
+
# No expiry
|
146
|
+
auth_result = AuthResult(
|
147
|
+
is_valid=True, status=AuthStatus.SUCCESS, token_expiry=None
|
148
|
+
)
|
149
|
+
|
150
|
+
assert auth_result.is_expired is False
|
151
|
+
assert auth_result.expires_soon is False
|
152
|
+
|
153
|
+
|
154
|
+
class TestValidationResult:
|
155
|
+
"""Test suite for ValidationResult class."""
|
156
|
+
|
157
|
+
def test_validation_result_success(self):
|
158
|
+
"""Test ValidationResult with successful validation."""
|
159
|
+
validation_result = ValidationResult(
|
160
|
+
is_valid=True,
|
161
|
+
status=ValidationStatus.VALID,
|
162
|
+
field_name="username",
|
163
|
+
value="testuser",
|
164
|
+
)
|
165
|
+
|
166
|
+
assert validation_result.is_valid is True
|
167
|
+
assert validation_result.status == ValidationStatus.VALID
|
168
|
+
assert validation_result.field_name == "username"
|
169
|
+
assert validation_result.value == "testuser"
|
170
|
+
assert validation_result.error_code is None
|
171
|
+
assert validation_result.error_message is None
|
172
|
+
assert validation_result.warnings == []
|
173
|
+
|
174
|
+
def test_validation_result_failure(self):
|
175
|
+
"""Test ValidationResult with failed validation."""
|
176
|
+
validation_result = ValidationResult(
|
177
|
+
is_valid=False,
|
178
|
+
status=ValidationStatus.INVALID,
|
179
|
+
field_name="password",
|
180
|
+
value="",
|
181
|
+
error_code=400,
|
182
|
+
error_message="Password cannot be empty",
|
183
|
+
warnings=["Password is too weak"],
|
184
|
+
)
|
185
|
+
|
186
|
+
assert validation_result.is_valid is False
|
187
|
+
assert validation_result.status == ValidationStatus.INVALID
|
188
|
+
assert validation_result.field_name == "password"
|
189
|
+
assert validation_result.value == ""
|
190
|
+
assert validation_result.error_code == 400
|
191
|
+
assert validation_result.error_message == "Password cannot be empty"
|
192
|
+
assert validation_result.warnings == ["Password is too weak"]
|
193
|
+
|
194
|
+
def test_validation_result_validation_consistency_success_with_error(self):
|
195
|
+
"""Test ValidationResult validation when success has error code."""
|
196
|
+
with pytest.raises(ValidationError) as exc_info:
|
197
|
+
ValidationResult(
|
198
|
+
is_valid=True, status=ValidationStatus.VALID, error_code=400
|
199
|
+
)
|
200
|
+
|
201
|
+
assert "Valid validation cannot have error code" in str(exc_info.value)
|
202
|
+
|
203
|
+
def test_validation_result_validation_consistency_failure_without_error(self):
|
204
|
+
"""Test ValidationResult validation when failure has no error."""
|
205
|
+
with pytest.raises(ValidationError) as exc_info:
|
206
|
+
ValidationResult(is_valid=False, status=ValidationStatus.VALID)
|
207
|
+
|
208
|
+
assert "Invalid validation cannot have VALID status" in str(exc_info.value)
|
209
|
+
|
210
|
+
|
211
|
+
class TestCertificateInfo:
|
212
|
+
"""Test suite for CertificateInfo class."""
|
213
|
+
|
214
|
+
def test_certificate_info_basic(self):
|
215
|
+
"""Test CertificateInfo with basic information."""
|
216
|
+
cert_info = CertificateInfo(
|
217
|
+
subject={"CN": "test.example.com", "O": "Test Org"},
|
218
|
+
issuer={"CN": "Test CA", "O": "Test CA Org"},
|
219
|
+
serial_number="123456789",
|
220
|
+
not_before=datetime.now(timezone.utc),
|
221
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
222
|
+
certificate_type=CertificateType.SERVER,
|
223
|
+
key_size=2048,
|
224
|
+
signature_algorithm="sha256WithRSAEncryption",
|
225
|
+
)
|
226
|
+
|
227
|
+
assert cert_info.subject == {"CN": "test.example.com", "O": "Test Org"}
|
228
|
+
assert cert_info.issuer == {"CN": "Test CA", "O": "Test CA Org"}
|
229
|
+
assert cert_info.serial_number == "123456789"
|
230
|
+
assert cert_info.certificate_type == CertificateType.SERVER
|
231
|
+
assert cert_info.key_size == 2048
|
232
|
+
assert cert_info.signature_algorithm == "sha256WithRSAEncryption"
|
233
|
+
assert cert_info.subject_alt_names == []
|
234
|
+
assert cert_info.key_usage == []
|
235
|
+
assert cert_info.extended_key_usage == []
|
236
|
+
assert cert_info.is_ca is False
|
237
|
+
assert cert_info.roles == []
|
238
|
+
assert cert_info.permissions == []
|
239
|
+
|
240
|
+
def test_certificate_info_complete(self):
|
241
|
+
"""Test CertificateInfo with complete information."""
|
242
|
+
cert_info = CertificateInfo(
|
243
|
+
subject={"CN": "test.example.com", "O": "Test Org", "OU": "IT"},
|
244
|
+
issuer={"CN": "Test CA", "O": "Test CA Org"},
|
245
|
+
serial_number="123456789",
|
246
|
+
not_before=datetime.now(timezone.utc),
|
247
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
248
|
+
certificate_type=CertificateType.CLIENT,
|
249
|
+
key_size=4096,
|
250
|
+
signature_algorithm="sha384WithRSAEncryption",
|
251
|
+
subject_alt_names=["*.example.com", "example.com"],
|
252
|
+
key_usage=["digitalSignature", "keyEncipherment"],
|
253
|
+
extended_key_usage=["clientAuth"],
|
254
|
+
is_ca=False,
|
255
|
+
path_length=None,
|
256
|
+
roles=["developer", "admin"],
|
257
|
+
permissions=["read", "write"],
|
258
|
+
certificate_path="/path/to/cert.pem",
|
259
|
+
fingerprint_sha1="abcd1234",
|
260
|
+
fingerprint_sha256="abcd12345678",
|
261
|
+
)
|
262
|
+
|
263
|
+
assert cert_info.subject_alt_names == ["*.example.com", "example.com"]
|
264
|
+
assert cert_info.key_usage == ["digitalSignature", "keyEncipherment"]
|
265
|
+
assert cert_info.extended_key_usage == ["clientAuth"]
|
266
|
+
assert cert_info.is_ca is False
|
267
|
+
assert cert_info.roles == ["developer", "admin"]
|
268
|
+
assert cert_info.permissions == ["read", "write"]
|
269
|
+
assert cert_info.certificate_path == "/path/to/cert.pem"
|
270
|
+
assert cert_info.fingerprint_sha1 == "abcd1234"
|
271
|
+
assert cert_info.fingerprint_sha256 == "abcd12345678"
|
272
|
+
|
273
|
+
def test_certificate_info_invalid_key_size(self):
|
274
|
+
"""Test CertificateInfo with invalid key size."""
|
275
|
+
with pytest.raises(ValidationError) as exc_info:
|
276
|
+
CertificateInfo(
|
277
|
+
subject={"CN": "test.example.com"},
|
278
|
+
issuer={"CN": "Test CA"},
|
279
|
+
serial_number="123456789",
|
280
|
+
not_before=datetime.now(timezone.utc),
|
281
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
282
|
+
certificate_type=CertificateType.SERVER,
|
283
|
+
key_size=256, # Too small
|
284
|
+
signature_algorithm="sha256WithRSAEncryption",
|
285
|
+
)
|
286
|
+
|
287
|
+
assert "Key size must be between 512 and 8192 bits" in str(exc_info.value)
|
288
|
+
|
289
|
+
def test_certificate_info_properties(self):
|
290
|
+
"""Test CertificateInfo properties."""
|
291
|
+
# Not expired
|
292
|
+
cert_info = CertificateInfo(
|
293
|
+
subject={"CN": "test.example.com"},
|
294
|
+
issuer={"CN": "Test CA"},
|
295
|
+
serial_number="123456789",
|
296
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=30),
|
297
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=60),
|
298
|
+
certificate_type=CertificateType.SERVER,
|
299
|
+
key_size=2048,
|
300
|
+
signature_algorithm="sha256WithRSAEncryption",
|
301
|
+
)
|
302
|
+
|
303
|
+
assert cert_info.is_expired is False
|
304
|
+
assert cert_info.expires_soon is False
|
305
|
+
assert cert_info.days_until_expiry > 0
|
306
|
+
assert cert_info.common_name == "test.example.com"
|
307
|
+
assert cert_info.organization is None
|
308
|
+
|
309
|
+
# Expires soon
|
310
|
+
cert_info = CertificateInfo(
|
311
|
+
subject={"CN": "test.example.com", "O": "Test Org"},
|
312
|
+
issuer={"CN": "Test CA"},
|
313
|
+
serial_number="123456789",
|
314
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=340),
|
315
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=20),
|
316
|
+
certificate_type=CertificateType.SERVER,
|
317
|
+
key_size=2048,
|
318
|
+
signature_algorithm="sha256WithRSAEncryption",
|
319
|
+
)
|
320
|
+
|
321
|
+
assert cert_info.is_expired is False
|
322
|
+
assert cert_info.expires_soon is True
|
323
|
+
assert cert_info.organization == "Test Org"
|
324
|
+
|
325
|
+
# Expired
|
326
|
+
cert_info = CertificateInfo(
|
327
|
+
subject={"CN": "test.example.com"},
|
328
|
+
issuer={"CN": "Test CA"},
|
329
|
+
serial_number="123456789",
|
330
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=400),
|
331
|
+
not_after=datetime.now(timezone.utc) - timedelta(days=10),
|
332
|
+
certificate_type=CertificateType.SERVER,
|
333
|
+
key_size=2048,
|
334
|
+
signature_algorithm="sha256WithRSAEncryption",
|
335
|
+
)
|
336
|
+
|
337
|
+
assert cert_info.is_expired is True
|
338
|
+
assert cert_info.days_until_expiry == 0
|
339
|
+
|
340
|
+
|
341
|
+
class TestCertificatePair:
|
342
|
+
"""Test suite for CertificatePair class."""
|
343
|
+
|
344
|
+
def test_certificate_pair_basic(self):
|
345
|
+
"""Test CertificatePair with basic information."""
|
346
|
+
cert_pair = CertificatePair(
|
347
|
+
certificate_path="/path/to/cert.pem",
|
348
|
+
private_key_path="/path/to/key.pem",
|
349
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----",
|
350
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----",
|
351
|
+
serial_number="123456789",
|
352
|
+
common_name="test.example.com",
|
353
|
+
organization="Test Org",
|
354
|
+
not_before=datetime.now(timezone.utc),
|
355
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
356
|
+
certificate_type=CertificateType.SERVER,
|
357
|
+
key_size=2048,
|
358
|
+
)
|
359
|
+
|
360
|
+
assert cert_pair.certificate_path == "/path/to/cert.pem"
|
361
|
+
assert cert_pair.private_key_path == "/path/to/key.pem"
|
362
|
+
assert cert_pair.serial_number == "123456789"
|
363
|
+
assert cert_pair.common_name == "test.example.com"
|
364
|
+
assert cert_pair.organization == "Test Org"
|
365
|
+
assert cert_pair.certificate_type == CertificateType.SERVER
|
366
|
+
assert cert_pair.key_size == 2048
|
367
|
+
assert cert_pair.roles == []
|
368
|
+
assert cert_pair.permissions == []
|
369
|
+
|
370
|
+
def test_certificate_pair_invalid_certificate_pem(self):
|
371
|
+
"""Test CertificatePair with invalid certificate PEM."""
|
372
|
+
with pytest.raises(ValidationError) as exc_info:
|
373
|
+
CertificatePair(
|
374
|
+
certificate_path="/path/to/cert.pem",
|
375
|
+
private_key_path="/path/to/key.pem",
|
376
|
+
certificate_pem="INVALID_CERT",
|
377
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----",
|
378
|
+
serial_number="123456789",
|
379
|
+
common_name="test.example.com",
|
380
|
+
organization="Test Org",
|
381
|
+
not_before=datetime.now(timezone.utc),
|
382
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
383
|
+
certificate_type=CertificateType.SERVER,
|
384
|
+
key_size=2048,
|
385
|
+
)
|
386
|
+
|
387
|
+
assert "Invalid certificate PEM format" in str(exc_info.value)
|
388
|
+
|
389
|
+
def test_certificate_pair_invalid_private_key_pem(self):
|
390
|
+
"""Test CertificatePair with invalid private key PEM."""
|
391
|
+
with pytest.raises(ValidationError) as exc_info:
|
392
|
+
CertificatePair(
|
393
|
+
certificate_path="/path/to/cert.pem",
|
394
|
+
private_key_path="/path/to/key.pem",
|
395
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----",
|
396
|
+
private_key_pem="INVALID_KEY",
|
397
|
+
serial_number="123456789",
|
398
|
+
common_name="test.example.com",
|
399
|
+
organization="Test Org",
|
400
|
+
not_before=datetime.now(timezone.utc),
|
401
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
402
|
+
certificate_type=CertificateType.SERVER,
|
403
|
+
key_size=2048,
|
404
|
+
)
|
405
|
+
|
406
|
+
assert "Invalid private key PEM format" in str(exc_info.value)
|
407
|
+
|
408
|
+
def test_certificate_pair_rsa_private_key_pem(self):
|
409
|
+
"""Test CertificatePair with RSA private key PEM format."""
|
410
|
+
cert_pair = CertificatePair(
|
411
|
+
certificate_path="/path/to/cert.pem",
|
412
|
+
private_key_path="/path/to/key.pem",
|
413
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----",
|
414
|
+
private_key_pem=(
|
415
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMII...\n-----END RSA PRIVATE KEY-----"
|
416
|
+
),
|
417
|
+
serial_number="123456789",
|
418
|
+
common_name="test.example.com",
|
419
|
+
organization="Test Org",
|
420
|
+
not_before=datetime.now(timezone.utc),
|
421
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
422
|
+
certificate_type=CertificateType.SERVER,
|
423
|
+
key_size=2048,
|
424
|
+
)
|
425
|
+
|
426
|
+
expected_key = (
|
427
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMII...\n-----END RSA PRIVATE KEY-----"
|
428
|
+
)
|
429
|
+
assert cert_pair.private_key_pem == expected_key
|
430
|
+
|
431
|
+
def test_certificate_pair_properties(self):
|
432
|
+
"""Test CertificatePair properties."""
|
433
|
+
# Not expired
|
434
|
+
cert_pair = CertificatePair(
|
435
|
+
certificate_path="/path/to/cert.pem",
|
436
|
+
private_key_path="/path/to/key.pem",
|
437
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----",
|
438
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----",
|
439
|
+
serial_number="123456789",
|
440
|
+
common_name="test.example.com",
|
441
|
+
organization="Test Org",
|
442
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=30),
|
443
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=60),
|
444
|
+
certificate_type=CertificateType.SERVER,
|
445
|
+
key_size=2048,
|
446
|
+
)
|
447
|
+
|
448
|
+
assert cert_pair.is_expired is False
|
449
|
+
assert cert_pair.expires_soon is False
|
450
|
+
|
451
|
+
# Expires soon
|
452
|
+
cert_pair = CertificatePair(
|
453
|
+
certificate_path="/path/to/cert.pem",
|
454
|
+
private_key_path="/path/to/key.pem",
|
455
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----",
|
456
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----",
|
457
|
+
serial_number="123456789",
|
458
|
+
common_name="test.example.com",
|
459
|
+
organization="Test Org",
|
460
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=340),
|
461
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=20),
|
462
|
+
certificate_type=CertificateType.SERVER,
|
463
|
+
key_size=2048,
|
464
|
+
)
|
465
|
+
|
466
|
+
assert cert_pair.is_expired is False
|
467
|
+
assert cert_pair.expires_soon is True
|
468
|
+
|
469
|
+
|
470
|
+
class TestRateLimitStatus:
|
471
|
+
"""Test suite for RateLimitStatus class."""
|
472
|
+
|
473
|
+
def test_rate_limit_status_basic(self):
|
474
|
+
"""Test RateLimitStatus with basic information."""
|
475
|
+
now = datetime.now(timezone.utc)
|
476
|
+
rate_limit_status = RateLimitStatus(
|
477
|
+
identifier="192.168.1.1",
|
478
|
+
current_count=5,
|
479
|
+
limit=10,
|
480
|
+
window_start=now,
|
481
|
+
window_end=now + timedelta(minutes=1),
|
482
|
+
is_exceeded=False,
|
483
|
+
remaining_requests=5,
|
484
|
+
reset_time=now + timedelta(minutes=1),
|
485
|
+
window_size_seconds=60,
|
486
|
+
)
|
487
|
+
|
488
|
+
assert rate_limit_status.identifier == "192.168.1.1"
|
489
|
+
assert rate_limit_status.current_count == 5
|
490
|
+
assert rate_limit_status.limit == 10
|
491
|
+
assert rate_limit_status.is_exceeded is False
|
492
|
+
assert rate_limit_status.remaining_requests == 5
|
493
|
+
assert rate_limit_status.window_size_seconds == 60
|
494
|
+
|
495
|
+
def test_rate_limit_status_exceeded(self):
|
496
|
+
"""Test RateLimitStatus when limit is exceeded."""
|
497
|
+
now = datetime.now(timezone.utc)
|
498
|
+
rate_limit_status = RateLimitStatus(
|
499
|
+
identifier="192.168.1.1",
|
500
|
+
current_count=15,
|
501
|
+
limit=10,
|
502
|
+
window_start=now,
|
503
|
+
window_end=now + timedelta(minutes=1),
|
504
|
+
is_exceeded=True,
|
505
|
+
remaining_requests=0,
|
506
|
+
reset_time=now + timedelta(minutes=1),
|
507
|
+
window_size_seconds=60,
|
508
|
+
)
|
509
|
+
|
510
|
+
assert rate_limit_status.is_exceeded is True
|
511
|
+
assert rate_limit_status.remaining_requests == 0
|
512
|
+
|
513
|
+
def test_rate_limit_status_validation_consistency(self):
|
514
|
+
"""Test RateLimitStatus validation consistency."""
|
515
|
+
now = datetime.now(timezone.utc)
|
516
|
+
|
517
|
+
# Exceeded but is_exceeded is False
|
518
|
+
with pytest.raises(ValidationError) as exc_info:
|
519
|
+
RateLimitStatus(
|
520
|
+
identifier="192.168.1.1",
|
521
|
+
current_count=15,
|
522
|
+
limit=10,
|
523
|
+
window_start=now,
|
524
|
+
window_end=now + timedelta(minutes=1),
|
525
|
+
is_exceeded=False, # Should be True
|
526
|
+
remaining_requests=0,
|
527
|
+
reset_time=now + timedelta(minutes=1),
|
528
|
+
window_size_seconds=60,
|
529
|
+
)
|
530
|
+
|
531
|
+
assert "Rate limit exceeded but is_exceeded is False" in str(exc_info.value)
|
532
|
+
|
533
|
+
# Not exceeded but is_exceeded is True
|
534
|
+
with pytest.raises(ValidationError) as exc_info:
|
535
|
+
RateLimitStatus(
|
536
|
+
identifier="192.168.1.1",
|
537
|
+
current_count=5,
|
538
|
+
limit=10,
|
539
|
+
window_start=now,
|
540
|
+
window_end=now + timedelta(minutes=1),
|
541
|
+
is_exceeded=True, # Should be False
|
542
|
+
remaining_requests=5,
|
543
|
+
reset_time=now + timedelta(minutes=1),
|
544
|
+
window_size_seconds=60,
|
545
|
+
)
|
546
|
+
|
547
|
+
assert "Rate limit not exceeded but is_exceeded is True" in str(exc_info.value)
|
548
|
+
|
549
|
+
def test_rate_limit_status_properties(self):
|
550
|
+
"""Test RateLimitStatus properties."""
|
551
|
+
now = datetime.now(timezone.utc)
|
552
|
+
rate_limit_status = RateLimitStatus(
|
553
|
+
identifier="192.168.1.1",
|
554
|
+
current_count=5,
|
555
|
+
limit=10,
|
556
|
+
window_start=now,
|
557
|
+
window_end=now + timedelta(minutes=1),
|
558
|
+
is_exceeded=False,
|
559
|
+
remaining_requests=5,
|
560
|
+
reset_time=now + timedelta(minutes=1),
|
561
|
+
window_size_seconds=60,
|
562
|
+
)
|
563
|
+
|
564
|
+
assert rate_limit_status.utilization_percentage == 50.0
|
565
|
+
assert rate_limit_status.seconds_until_reset > 0
|
566
|
+
|
567
|
+
# 100% utilization
|
568
|
+
rate_limit_status = RateLimitStatus(
|
569
|
+
identifier="192.168.1.1",
|
570
|
+
current_count=10,
|
571
|
+
limit=10,
|
572
|
+
window_start=now,
|
573
|
+
window_end=now + timedelta(minutes=1),
|
574
|
+
is_exceeded=False,
|
575
|
+
remaining_requests=0,
|
576
|
+
reset_time=now + timedelta(minutes=1),
|
577
|
+
window_size_seconds=60,
|
578
|
+
)
|
579
|
+
|
580
|
+
assert rate_limit_status.utilization_percentage == 100.0
|
581
|
+
|
582
|
+
# 0% utilization
|
583
|
+
rate_limit_status = RateLimitStatus(
|
584
|
+
identifier="192.168.1.1",
|
585
|
+
current_count=0,
|
586
|
+
limit=10,
|
587
|
+
window_start=now,
|
588
|
+
window_end=now + timedelta(minutes=1),
|
589
|
+
is_exceeded=False,
|
590
|
+
remaining_requests=10,
|
591
|
+
reset_time=now + timedelta(minutes=1),
|
592
|
+
window_size_seconds=60,
|
593
|
+
)
|
594
|
+
|
595
|
+
assert rate_limit_status.utilization_percentage == 0.0
|
596
|
+
|
597
|
+
|
598
|
+
class TestUserCredentials:
|
599
|
+
"""Test suite for UserCredentials class."""
|
600
|
+
|
601
|
+
def test_user_credentials_basic(self):
|
602
|
+
"""Test UserCredentials with basic information."""
|
603
|
+
user_creds = UserCredentials(
|
604
|
+
username="testuser",
|
605
|
+
password="hashed_password",
|
606
|
+
api_key="api_key_123",
|
607
|
+
certificate_path="/path/to/cert.pem",
|
608
|
+
roles=["user", "admin"],
|
609
|
+
permissions={"read", "write"},
|
610
|
+
)
|
611
|
+
|
612
|
+
assert user_creds.username == "testuser"
|
613
|
+
assert user_creds.password == "hashed_password"
|
614
|
+
assert user_creds.api_key == "api_key_123"
|
615
|
+
assert user_creds.certificate_path == "/path/to/cert.pem"
|
616
|
+
assert user_creds.roles == ["user", "admin"]
|
617
|
+
assert user_creds.permissions == {"read", "write"}
|
618
|
+
assert user_creds.is_active is True
|
619
|
+
assert user_creds.last_login is None
|
620
|
+
|
621
|
+
def test_user_credentials_username_validation(self):
|
622
|
+
"""Test UserCredentials username validation."""
|
623
|
+
# Valid username
|
624
|
+
user_creds = UserCredentials(username="testuser")
|
625
|
+
assert user_creds.username == "testuser"
|
626
|
+
|
627
|
+
# Username with whitespace
|
628
|
+
user_creds = UserCredentials(username=" testuser ")
|
629
|
+
assert user_creds.username == "testuser"
|
630
|
+
|
631
|
+
# Empty username
|
632
|
+
with pytest.raises(ValidationError) as exc_info:
|
633
|
+
UserCredentials(username="")
|
634
|
+
|
635
|
+
assert "Username cannot be empty" in str(exc_info.value)
|
636
|
+
|
637
|
+
# Username too long
|
638
|
+
long_username = "a" * 101
|
639
|
+
with pytest.raises(ValidationError) as exc_info:
|
640
|
+
UserCredentials(username=long_username)
|
641
|
+
|
642
|
+
assert "Username too long" in str(exc_info.value)
|
643
|
+
|
644
|
+
def test_user_credentials_properties(self):
|
645
|
+
"""Test UserCredentials properties."""
|
646
|
+
# User with password
|
647
|
+
user_creds = UserCredentials(username="testuser", password="hashed_password")
|
648
|
+
|
649
|
+
assert user_creds.has_password is True
|
650
|
+
assert user_creds.has_api_key is False
|
651
|
+
assert user_creds.has_certificate is False
|
652
|
+
|
653
|
+
# User with API key
|
654
|
+
user_creds = UserCredentials(username="testuser", api_key="api_key_123")
|
655
|
+
|
656
|
+
assert user_creds.has_password is False
|
657
|
+
assert user_creds.has_api_key is True
|
658
|
+
assert user_creds.has_certificate is False
|
659
|
+
|
660
|
+
# User with certificate
|
661
|
+
user_creds = UserCredentials(
|
662
|
+
username="testuser", certificate_path="/path/to/cert.pem"
|
663
|
+
)
|
664
|
+
|
665
|
+
assert user_creds.has_password is False
|
666
|
+
assert user_creds.has_api_key is False
|
667
|
+
assert user_creds.has_certificate is True
|
668
|
+
|
669
|
+
# User with empty password
|
670
|
+
user_creds = UserCredentials(username="testuser", password="")
|
671
|
+
|
672
|
+
assert user_creds.has_password is False
|
673
|
+
|
674
|
+
|
675
|
+
class TestRolePermissions:
|
676
|
+
"""Test suite for RolePermissions class."""
|
677
|
+
|
678
|
+
def test_role_permissions_basic(self):
|
679
|
+
"""Test RolePermissions with basic information."""
|
680
|
+
role_perms = RolePermissions(
|
681
|
+
role_name="admin",
|
682
|
+
permissions={"read", "write", "delete"},
|
683
|
+
parent_roles=["user"],
|
684
|
+
child_roles=["moderator"],
|
685
|
+
description="Administrator role with full access",
|
686
|
+
)
|
687
|
+
|
688
|
+
assert role_perms.role_name == "admin"
|
689
|
+
assert role_perms.permissions == {"read", "write", "delete"}
|
690
|
+
assert role_perms.parent_roles == ["user"]
|
691
|
+
assert role_perms.child_roles == ["moderator"]
|
692
|
+
assert role_perms.description == "Administrator role with full access"
|
693
|
+
assert role_perms.is_active is True
|
694
|
+
|
695
|
+
def test_role_permissions_username_validation(self):
|
696
|
+
"""Test RolePermissions role name validation."""
|
697
|
+
# Valid role name
|
698
|
+
role_perms = RolePermissions(role_name="admin")
|
699
|
+
assert role_perms.role_name == "admin"
|
700
|
+
|
701
|
+
# Role name with whitespace
|
702
|
+
role_perms = RolePermissions(role_name=" admin ")
|
703
|
+
assert role_perms.role_name == "admin"
|
704
|
+
|
705
|
+
# Empty role name
|
706
|
+
with pytest.raises(ValidationError) as exc_info:
|
707
|
+
RolePermissions(role_name="")
|
708
|
+
|
709
|
+
assert "Role name cannot be empty" in str(exc_info.value)
|
710
|
+
|
711
|
+
# Role name too long
|
712
|
+
long_role_name = "a" * 101
|
713
|
+
with pytest.raises(ValidationError) as exc_info:
|
714
|
+
RolePermissions(role_name=long_role_name)
|
715
|
+
|
716
|
+
assert "Role name too long" in str(exc_info.value)
|
717
|
+
|
718
|
+
def test_role_permissions_properties(self):
|
719
|
+
"""Test RolePermissions properties."""
|
720
|
+
role_perms = RolePermissions(
|
721
|
+
role_name="admin", permissions={"read", "write", "delete"}
|
722
|
+
)
|
723
|
+
|
724
|
+
# Test effective permissions (currently returns direct permissions)
|
725
|
+
effective_perms = role_perms.effective_permissions
|
726
|
+
assert effective_perms == {"read", "write", "delete"}
|
727
|
+
assert effective_perms is not role_perms.permissions # Should be a copy
|
728
|
+
|
729
|
+
# Test has_permission
|
730
|
+
assert role_perms.has_permission("read") is True
|
731
|
+
assert role_perms.has_permission("write") is True
|
732
|
+
assert role_perms.has_permission("delete") is True
|
733
|
+
assert role_perms.has_permission("execute") is False
|
734
|
+
|
735
|
+
|
736
|
+
class TestCertificateChain:
|
737
|
+
"""Test suite for CertificateChain class."""
|
738
|
+
|
739
|
+
def test_certificate_chain_basic(self):
|
740
|
+
"""Test CertificateChain with basic information."""
|
741
|
+
# Create mock certificates
|
742
|
+
end_entity = CertificateInfo(
|
743
|
+
subject={"CN": "test.example.com"},
|
744
|
+
issuer={"CN": "Intermediate CA"},
|
745
|
+
serial_number="123456789",
|
746
|
+
not_before=datetime.now(timezone.utc),
|
747
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
748
|
+
certificate_type=CertificateType.SERVER,
|
749
|
+
key_size=2048,
|
750
|
+
signature_algorithm="sha256WithRSAEncryption",
|
751
|
+
)
|
752
|
+
|
753
|
+
intermediate = CertificateInfo(
|
754
|
+
subject={"CN": "Intermediate CA"},
|
755
|
+
issuer={"CN": "Root CA"},
|
756
|
+
serial_number="987654321",
|
757
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=365),
|
758
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
759
|
+
certificate_type=CertificateType.INTERMEDIATE_CA,
|
760
|
+
key_size=4096,
|
761
|
+
signature_algorithm="sha256WithRSAEncryption",
|
762
|
+
is_ca=True,
|
763
|
+
)
|
764
|
+
|
765
|
+
root = CertificateInfo(
|
766
|
+
subject={"CN": "Root CA"},
|
767
|
+
issuer={"CN": "Root CA"},
|
768
|
+
serial_number="111111111",
|
769
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=730),
|
770
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=3650),
|
771
|
+
certificate_type=CertificateType.ROOT_CA,
|
772
|
+
key_size=4096,
|
773
|
+
signature_algorithm="sha256WithRSAEncryption",
|
774
|
+
is_ca=True,
|
775
|
+
)
|
776
|
+
|
777
|
+
cert_chain = CertificateChain(
|
778
|
+
certificates=[end_entity, intermediate, root],
|
779
|
+
chain_length=3,
|
780
|
+
is_valid=True,
|
781
|
+
root_ca=root,
|
782
|
+
intermediate_cas=[intermediate],
|
783
|
+
end_entity=end_entity,
|
784
|
+
)
|
785
|
+
|
786
|
+
assert cert_chain.chain_length == 3
|
787
|
+
assert cert_chain.is_valid is True
|
788
|
+
assert len(cert_chain.certificates) == 3
|
789
|
+
assert cert_chain.root_ca == root
|
790
|
+
assert cert_chain.intermediate_cas == [intermediate]
|
791
|
+
assert cert_chain.end_entity == end_entity
|
792
|
+
assert cert_chain.validation_errors == []
|
793
|
+
|
794
|
+
def test_certificate_chain_validation_consistency(self):
|
795
|
+
"""Test CertificateChain validation consistency."""
|
796
|
+
# Create mock certificate
|
797
|
+
cert = CertificateInfo(
|
798
|
+
subject={"CN": "test.example.com"},
|
799
|
+
issuer={"CN": "Test CA"},
|
800
|
+
serial_number="123456789",
|
801
|
+
not_before=datetime.now(timezone.utc),
|
802
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
803
|
+
certificate_type=CertificateType.SERVER,
|
804
|
+
key_size=2048,
|
805
|
+
signature_algorithm="sha256WithRSAEncryption",
|
806
|
+
)
|
807
|
+
|
808
|
+
# Chain length mismatch
|
809
|
+
with pytest.raises(ValidationError) as exc_info:
|
810
|
+
CertificateChain(
|
811
|
+
certificates=[cert], chain_length=2, is_valid=True
|
812
|
+
) # Should be 1
|
813
|
+
|
814
|
+
assert "Chain length must match number of certificates" in str(exc_info.value)
|
815
|
+
|
816
|
+
def test_certificate_chain_properties(self):
|
817
|
+
"""Test CertificateChain properties."""
|
818
|
+
# Create mock certificates
|
819
|
+
end_entity = CertificateInfo(
|
820
|
+
subject={"CN": "test.example.com"},
|
821
|
+
issuer={"CN": "Intermediate CA"},
|
822
|
+
serial_number="123456789",
|
823
|
+
not_before=datetime.now(timezone.utc),
|
824
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
825
|
+
certificate_type=CertificateType.SERVER,
|
826
|
+
key_size=2048,
|
827
|
+
signature_algorithm="sha256WithRSAEncryption",
|
828
|
+
)
|
829
|
+
|
830
|
+
intermediate = CertificateInfo(
|
831
|
+
subject={"CN": "Intermediate CA"},
|
832
|
+
issuer={"CN": "Root CA"},
|
833
|
+
serial_number="987654321",
|
834
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=365),
|
835
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
836
|
+
certificate_type=CertificateType.INTERMEDIATE_CA,
|
837
|
+
key_size=4096,
|
838
|
+
signature_algorithm="sha256WithRSAEncryption",
|
839
|
+
is_ca=True,
|
840
|
+
)
|
841
|
+
|
842
|
+
root = CertificateInfo(
|
843
|
+
subject={"CN": "Root CA"},
|
844
|
+
issuer={"CN": "Root CA"},
|
845
|
+
serial_number="111111111",
|
846
|
+
not_before=datetime.now(timezone.utc) - timedelta(days=730),
|
847
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=3650),
|
848
|
+
certificate_type=CertificateType.ROOT_CA,
|
849
|
+
key_size=4096,
|
850
|
+
signature_algorithm="sha256WithRSAEncryption",
|
851
|
+
is_ca=True,
|
852
|
+
)
|
853
|
+
|
854
|
+
# Chain with intermediate CAs
|
855
|
+
cert_chain = CertificateChain(
|
856
|
+
certificates=[end_entity, intermediate, root], chain_length=3, is_valid=True
|
857
|
+
)
|
858
|
+
|
859
|
+
assert cert_chain.has_intermediate_cas is True
|
860
|
+
assert cert_chain.is_self_signed is False
|
861
|
+
|
862
|
+
# Self-signed certificate
|
863
|
+
self_signed = CertificateInfo(
|
864
|
+
subject={"CN": "test.example.com"},
|
865
|
+
issuer={"CN": "test.example.com"},
|
866
|
+
serial_number="123456789",
|
867
|
+
not_before=datetime.now(timezone.utc),
|
868
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
869
|
+
certificate_type=CertificateType.SERVER,
|
870
|
+
key_size=2048,
|
871
|
+
signature_algorithm="sha256WithRSAEncryption",
|
872
|
+
)
|
873
|
+
|
874
|
+
cert_chain = CertificateChain(
|
875
|
+
certificates=[self_signed], chain_length=1, is_valid=True
|
876
|
+
)
|
877
|
+
|
878
|
+
assert cert_chain.has_intermediate_cas is False
|
879
|
+
assert cert_chain.is_self_signed is True
|