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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. 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