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,477 @@
1
+ """
2
+ Validation Utilities Test Module
3
+
4
+ This module provides comprehensive unit tests for all validation
5
+ utilities in the MCP Security Framework.
6
+
7
+ Test Classes:
8
+ TestInputValidation: Tests for input data validation
9
+ TestFormatValidation: Tests for format validation
10
+ TestDataNormalization: Tests for data normalization
11
+ TestFileValidation: Tests for file validation
12
+
13
+ Author: MCP Security Team
14
+ Version: 1.0.0
15
+ License: MIT
16
+ """
17
+
18
+ import pytest
19
+
20
+ from mcp_security_framework.utils.validation_utils import (
21
+ ValidationError,
22
+ normalize_data,
23
+ sanitize_string,
24
+ validate_configuration_file,
25
+ validate_directory_structure,
26
+ validate_email,
27
+ validate_file_extension,
28
+ validate_file_path,
29
+ validate_input_data,
30
+ validate_ip_address,
31
+ validate_json_schema,
32
+ validate_list_content,
33
+ validate_numeric_range,
34
+ validate_string_length,
35
+ validate_url,
36
+ )
37
+
38
+
39
+ class TestInputValidation:
40
+ """Test suite for input data validation."""
41
+
42
+ def test_validate_input_data_dict_success(self):
43
+ """Test successful dictionary validation."""
44
+ data = {"name": "test", "age": 25}
45
+ result = validate_input_data(data, required_fields=["name"], data_type=dict)
46
+ assert result is True
47
+
48
+ def test_validate_input_data_missing_required_fields(self):
49
+ """Test validation with missing required fields."""
50
+ data = {"name": "test"}
51
+ with pytest.raises(ValidationError) as exc_info:
52
+ validate_input_data(data, required_fields=["name", "age"])
53
+
54
+ assert "Missing required fields" in str(exc_info.value)
55
+
56
+ def test_validate_input_data_invalid_fields(self):
57
+ """Test validation with invalid fields."""
58
+ data = {"name": "test", "invalid_field": "value"}
59
+ with pytest.raises(ValidationError) as exc_info:
60
+ validate_input_data(data, allowed_fields=["name"])
61
+
62
+ assert "Invalid fields" in str(exc_info.value)
63
+
64
+ def test_validate_input_data_wrong_type(self):
65
+ """Test validation with wrong data type."""
66
+ data = "not_a_dict"
67
+ with pytest.raises(ValidationError) as exc_info:
68
+ validate_input_data(data, data_type=dict)
69
+
70
+ assert "Expected dict" in str(exc_info.value)
71
+
72
+ def test_validate_input_data_no_constraints(self):
73
+ """Test validation without any constraints."""
74
+ data = {"name": "test", "age": 25}
75
+ result = validate_input_data(data)
76
+ assert result is True
77
+
78
+ def test_validate_input_data_list_type(self):
79
+ """Test validation with list data type."""
80
+ data = [1, 2, 3]
81
+ result = validate_input_data(data, data_type=list)
82
+ assert result is True
83
+
84
+ def test_validate_input_data_string_type(self):
85
+ """Test validation with string data type."""
86
+ data = "test_string"
87
+ result = validate_input_data(data, data_type=str)
88
+ assert result is True
89
+
90
+
91
+ class TestFormatValidation:
92
+ """Test suite for format validation."""
93
+
94
+ def test_validate_email_valid(self):
95
+ """Test valid email validation."""
96
+ valid_emails = [
97
+ "test@example.com",
98
+ "user.name@domain.co.uk",
99
+ "user+tag@example.org",
100
+ ]
101
+
102
+ for email in valid_emails:
103
+ assert validate_email(email) is True
104
+
105
+ def test_validate_email_invalid(self):
106
+ """Test invalid email validation."""
107
+ invalid_emails = [
108
+ "",
109
+ "invalid_email",
110
+ "@example.com",
111
+ "test@",
112
+ "test..test@example.com",
113
+ ]
114
+
115
+ for email in invalid_emails:
116
+ assert validate_email(email) is False
117
+
118
+ def test_validate_email_none_input(self):
119
+ """Test email validation with None input."""
120
+ assert validate_email(None) is False
121
+
122
+ def test_validate_email_non_string_input(self):
123
+ """Test email validation with non-string input."""
124
+ assert validate_email(123) is False
125
+
126
+ def test_validate_url_valid(self):
127
+ """Test valid URL validation."""
128
+ valid_urls = [
129
+ "https://example.com",
130
+ "http://subdomain.example.org/path",
131
+ "ftp://ftp.example.net",
132
+ ]
133
+
134
+ for url in valid_urls:
135
+ assert validate_url(url) is True
136
+
137
+ def test_validate_url_invalid(self):
138
+ """Test invalid URL validation."""
139
+ invalid_urls = ["", "not_a_url", "http://", "https://"]
140
+
141
+ for url in invalid_urls:
142
+ assert validate_url(url) is False
143
+
144
+ def test_validate_url_allowed_schemes(self):
145
+ """Test URL validation with allowed schemes."""
146
+ url = "https://example.com"
147
+ assert validate_url(url, allowed_schemes=["https"]) is True
148
+ assert validate_url(url, allowed_schemes=["http"]) is False
149
+
150
+ def test_validate_url_none_input(self):
151
+ """Test URL validation with None input."""
152
+ assert validate_url(None) is False
153
+
154
+ def test_validate_url_non_string_input(self):
155
+ """Test URL validation with non-string input."""
156
+ assert validate_url(123) is False
157
+
158
+ def test_validate_ip_address_valid(self):
159
+ """Test valid IP address validation."""
160
+ valid_ips = ["192.168.1.1", "10.0.0.1", "127.0.0.1", "::1", "2001:db8::1"]
161
+
162
+ for ip in valid_ips:
163
+ assert validate_ip_address(ip) is True
164
+
165
+ def test_validate_ip_address_invalid(self):
166
+ """Test invalid IP address validation."""
167
+ invalid_ips = ["", "256.256.256.256", "192.168.1.256", "not_an_ip"]
168
+
169
+ for ip in invalid_ips:
170
+ assert validate_ip_address(ip) is False
171
+
172
+ def test_validate_ip_address_specific_version(self):
173
+ """Test IP address validation with specific version."""
174
+ assert validate_ip_address("192.168.1.1", "ipv4") is True
175
+ assert validate_ip_address("::1", "ipv6") is True
176
+ assert validate_ip_address("192.168.1.1", "ipv6") is False
177
+
178
+ def test_validate_ip_address_none_input(self):
179
+ """Test IP address validation with None input."""
180
+ assert validate_ip_address(None) is False
181
+
182
+ def test_validate_ip_address_non_string_input(self):
183
+ """Test IP address validation with non-string input."""
184
+ assert validate_ip_address(123) is False
185
+
186
+
187
+ class TestDataNormalization:
188
+ """Test suite for data normalization."""
189
+
190
+ def test_normalize_data_string(self):
191
+ """Test string normalization."""
192
+ assert normalize_data(" test ", "string") == "test"
193
+ assert normalize_data(None, "string") == ""
194
+ assert normalize_data(123, "string") == "123"
195
+
196
+ def test_normalize_data_integer(self):
197
+ """Test integer normalization."""
198
+ assert normalize_data("123", "integer") == 123
199
+ assert normalize_data(123.45, "integer") == 123
200
+ assert normalize_data(None, "integer") == 0
201
+
202
+ def test_normalize_data_float(self):
203
+ """Test float normalization."""
204
+ assert normalize_data("123.45", "float") == 123.45
205
+ assert normalize_data(123, "float") == 123.0
206
+ assert normalize_data(None, "float") == 0.0
207
+
208
+ def test_normalize_data_boolean(self):
209
+ """Test boolean normalization."""
210
+ assert normalize_data("true", "boolean") is True
211
+ assert normalize_data("false", "boolean") is False
212
+ assert normalize_data("1", "boolean") is True
213
+ assert normalize_data("0", "boolean") is False
214
+ assert normalize_data(None, "boolean") is False
215
+
216
+ def test_normalize_data_boolean_true_values(self):
217
+ """Test boolean normalization with various true values."""
218
+ assert normalize_data(True, "boolean") is True
219
+ assert normalize_data(1, "boolean") is True
220
+ assert normalize_data("yes", "boolean") is True
221
+ assert normalize_data("on", "boolean") is True
222
+
223
+ def test_normalize_data_boolean_false_values(self):
224
+ """Test boolean normalization with various false values."""
225
+ assert normalize_data(False, "boolean") is False
226
+ assert normalize_data(0, "boolean") is False
227
+ assert normalize_data("", "boolean") is False
228
+
229
+ def test_normalize_data_unsupported_type(self):
230
+ """Test normalization with unsupported type."""
231
+ with pytest.raises(ValidationError) as exc_info:
232
+ normalize_data("test", "unsupported")
233
+
234
+ assert "Unsupported data type" in str(exc_info.value)
235
+
236
+ def test_sanitize_string(self):
237
+ """Test string sanitization."""
238
+ assert sanitize_string(" test ") == "test"
239
+ assert (
240
+ sanitize_string("test<script>alert('xss')</script>") == "testalert('xss')"
241
+ )
242
+ assert sanitize_string("test\n\t\r") == "test"
243
+
244
+ def test_sanitize_string_custom_chars(self):
245
+ """Test string sanitization with custom allowed characters."""
246
+ result = sanitize_string("test123", r"[a-z]")
247
+ assert result == "test123"
248
+
249
+ def test_sanitize_string_none_input(self):
250
+ """Test string sanitization with None input."""
251
+ assert sanitize_string(None) == ""
252
+
253
+ def test_sanitize_string_non_string_input(self):
254
+ """Test string sanitization with non-string input."""
255
+ assert sanitize_string(123) == ""
256
+
257
+
258
+ class TestValidationFunctions:
259
+ """Test suite for validation functions."""
260
+
261
+ def test_validate_string_length(self):
262
+ """Test string length validation."""
263
+ assert validate_string_length("test", min_length=3, max_length=5) is True
264
+ assert validate_string_length("test", min_length=5) is False
265
+ assert validate_string_length("test", max_length=3) is False
266
+
267
+ def test_validate_string_length_none_input(self):
268
+ """Test string length validation with None input."""
269
+ assert validate_string_length(None, min_length=3) is False
270
+
271
+ def test_validate_string_length_non_string_input(self):
272
+ """Test string length validation with non-string input."""
273
+ assert validate_string_length(123, min_length=3) is False
274
+
275
+ def test_validate_numeric_range(self):
276
+ """Test numeric value range validation."""
277
+ assert validate_numeric_range(5, min_value=1, max_value=10) is True
278
+ assert validate_numeric_range(5, min_value=10) is False
279
+ assert validate_numeric_range(5, max_value=3) is False
280
+
281
+ def test_validate_numeric_range_none_input(self):
282
+ """Test numeric range validation with None input."""
283
+ assert validate_numeric_range(None, min_value=1) is False
284
+
285
+ def test_validate_numeric_range_non_numeric_input(self):
286
+ """Test numeric range validation with non-numeric input."""
287
+ assert validate_numeric_range("not_a_number", min_value=1) is False
288
+
289
+ def test_validate_list_content(self):
290
+ """Test list content and structure validation."""
291
+ assert validate_list_content([1, 2, 3], min_items=2, max_items=5) is True
292
+ assert validate_list_content([1, 2, 3], allowed_values=[1, 2, 3, 4]) is True
293
+ assert validate_list_content([1, 2, 3], allowed_values=[1, 2]) is False
294
+
295
+ def test_validate_list_content_none_input(self):
296
+ """Test list content validation with None input."""
297
+ assert validate_list_content(None, min_items=2) is False
298
+
299
+ def test_validate_list_content_non_list_input(self):
300
+ """Test list content validation with non-list input."""
301
+ assert validate_list_content("not_a_list", min_items=2) is False
302
+
303
+ def test_validate_list_content_too_few_items(self):
304
+ """Test list content validation with too few items."""
305
+ assert validate_list_content([1], min_items=2) is False
306
+
307
+ def test_validate_list_content_too_many_items(self):
308
+ """Test list content validation with too many items."""
309
+ assert validate_list_content([1, 2, 3], max_items=2) is False
310
+
311
+ def test_validate_json_schema_success(self):
312
+ """Test successful JSON schema validation."""
313
+ data = {"name": "test", "age": 25}
314
+ schema = {
315
+ "type": "object",
316
+ "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
317
+ "required": ["name"],
318
+ }
319
+
320
+ assert validate_json_schema(data, schema) is True
321
+
322
+ def test_validate_json_schema_missing_required(self):
323
+ """Test JSON schema validation with missing required field."""
324
+ data = {"age": 25}
325
+ schema = {
326
+ "type": "object",
327
+ "properties": {"name": {"type": "string"}, "age": {"type": "integer"}},
328
+ "required": ["name"],
329
+ }
330
+
331
+ with pytest.raises(ValidationError) as exc_info:
332
+ validate_json_schema(data, schema)
333
+
334
+ assert "Missing required field" in str(exc_info.value)
335
+
336
+ def test_validate_json_schema_wrong_type(self):
337
+ """Test JSON schema validation with wrong type."""
338
+ data = {"name": 123}
339
+ schema = {"type": "object", "properties": {"name": {"type": "string"}}}
340
+
341
+ with pytest.raises(ValidationError) as exc_info:
342
+ validate_json_schema(data, schema)
343
+
344
+ assert "must be a string" in str(exc_info.value)
345
+
346
+ def test_validate_json_schema_not_dict(self):
347
+ """Test JSON schema validation with non-dict data."""
348
+ data = "not_a_dict"
349
+ schema = {"type": "object"}
350
+
351
+ with pytest.raises(ValidationError) as exc_info:
352
+ validate_json_schema(data, schema)
353
+
354
+ assert "Data must be a dictionary" in str(exc_info.value)
355
+
356
+ def test_validate_json_schema_integer_type(self):
357
+ """Test JSON schema validation with integer type."""
358
+ data = {"age": 25}
359
+ schema = {"type": "object", "properties": {"age": {"type": "integer"}}}
360
+
361
+ assert validate_json_schema(data, schema) is True
362
+
363
+ def test_validate_json_schema_number_type(self):
364
+ """Test JSON schema validation with number type."""
365
+ data = {"price": 25.50}
366
+ schema = {"type": "object", "properties": {"price": {"type": "number"}}}
367
+
368
+ assert validate_json_schema(data, schema) is True
369
+
370
+ def test_validate_json_schema_boolean_type(self):
371
+ """Test JSON schema validation with boolean type."""
372
+ data = {"active": True}
373
+ schema = {"type": "object", "properties": {"active": {"type": "boolean"}}}
374
+
375
+ assert validate_json_schema(data, schema) is True
376
+
377
+ def test_validate_json_schema_array_type(self):
378
+ """Test JSON schema validation with array type."""
379
+ data = {"items": [1, 2, 3]}
380
+ schema = {"type": "object", "properties": {"items": {"type": "array"}}}
381
+
382
+ assert validate_json_schema(data, schema) is True
383
+
384
+ def test_validate_json_schema_object_type(self):
385
+ """Test JSON schema validation with object type."""
386
+ data = {"user": {"name": "test"}}
387
+ schema = {"type": "object", "properties": {"user": {"type": "object"}}}
388
+
389
+ assert validate_json_schema(data, schema) is True
390
+
391
+
392
+ class TestFileValidation:
393
+ """Test suite for file validation."""
394
+
395
+ def test_validate_file_extension(self):
396
+ """Test file extension validation."""
397
+ assert validate_file_extension("test.txt", [".txt", ".pdf"]) is True
398
+ assert validate_file_extension("test.txt", [".pdf", ".doc"]) is False
399
+
400
+ def test_validate_file_extension_case_insensitive(self):
401
+ """Test file extension validation with case insensitive."""
402
+ assert validate_file_extension("test.TXT", [".txt", ".pdf"]) is True
403
+ assert validate_file_extension("test.txt", [".TXT", ".PDF"]) is True
404
+
405
+ def test_validate_file_extension_without_dot(self):
406
+ """Test file extension validation without leading dot."""
407
+ assert validate_file_extension("test.txt", ["txt", "pdf"]) is True
408
+ assert validate_file_extension("test.txt", ["pdf", "doc"]) is False
409
+
410
+ def test_validate_file_extension_exception_handling(self):
411
+ """Test file extension validation with exception handling."""
412
+ assert validate_file_extension(None, [".txt"]) is False
413
+ assert validate_file_extension(123, [".txt"]) is False
414
+
415
+ def test_validate_file_path(self):
416
+ """Test file path validation."""
417
+ # Test with non-existent path
418
+ assert validate_file_path("/non/existent/path", must_exist=False) is True
419
+ assert validate_file_path("/non/existent/path", must_exist=True) is False
420
+
421
+ def test_validate_file_path_exception_handling(self):
422
+ """Test file path validation with exception handling."""
423
+ assert validate_file_path(None) is False
424
+ assert validate_file_path(123) is False
425
+
426
+ def test_validate_directory_structure(self):
427
+ """Test directory structure validation."""
428
+ # Test with non-existent directory
429
+ assert validate_directory_structure("/non/existent/dir") is False
430
+
431
+ def test_validate_directory_structure_exception_handling(self):
432
+ """Test directory structure validation with exception handling."""
433
+ assert validate_directory_structure(None) is False
434
+ assert validate_directory_structure(123) is False
435
+
436
+
437
+ class TestConfigurationValidation:
438
+ """Test suite for configuration file validation."""
439
+
440
+ def test_validate_configuration_file_not_found(self, tmp_path):
441
+ """Test configuration file validation with non-existent file."""
442
+ config_path = tmp_path / "nonexistent.json"
443
+
444
+ with pytest.raises(ValidationError) as exc_info:
445
+ validate_configuration_file(config_path, None)
446
+
447
+ assert "Configuration file not found" in str(exc_info.value)
448
+
449
+ def test_validate_configuration_file_not_file(self, tmp_path):
450
+ """Test configuration file validation with directory instead of file."""
451
+ config_dir = tmp_path / "config"
452
+ config_dir.mkdir()
453
+
454
+ with pytest.raises(ValidationError) as exc_info:
455
+ validate_configuration_file(config_dir, None)
456
+
457
+ assert "Path is not a file" in str(exc_info.value)
458
+
459
+ def test_validate_configuration_file_unsupported_format(self, tmp_path):
460
+ """Test configuration file validation with unsupported format."""
461
+ config_path = tmp_path / "config.yaml"
462
+ config_path.write_text("test: data")
463
+
464
+ with pytest.raises(ValidationError) as exc_info:
465
+ validate_configuration_file(config_path, None)
466
+
467
+ assert "Unsupported configuration file format" in str(exc_info.value)
468
+
469
+ def test_validate_configuration_file_invalid_json(self, tmp_path):
470
+ """Test configuration file validation with invalid JSON."""
471
+ config_path = tmp_path / "config.json"
472
+ config_path.write_text("{ invalid json }")
473
+
474
+ with pytest.raises(ValidationError) as exc_info:
475
+ validate_configuration_file(config_path, None)
476
+
477
+ assert "Invalid JSON in configuration file" in str(exc_info.value)