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,626 @@
1
+ """
2
+ Rate Limiter Tests Module
3
+
4
+ This module provides comprehensive tests for the RateLimiter class,
5
+ covering all functionality including rate limiting, window management,
6
+ cleanup, and edge cases.
7
+
8
+ Test Coverage:
9
+ - Basic rate limiting functionality
10
+ - Window reset behavior
11
+ - Multiple identifiers
12
+ - Exempt paths and roles
13
+ - Cleanup functionality
14
+ - Thread safety
15
+ - Edge cases and error conditions
16
+
17
+ Author: MCP Security Team
18
+ Version: 1.0.0
19
+ License: MIT
20
+ """
21
+
22
+ import time
23
+ from datetime import datetime, timedelta, timezone
24
+ from unittest.mock import Mock, patch
25
+
26
+ import pytest
27
+
28
+ from mcp_security_framework.core.rate_limiter import RateLimitEntry, RateLimiter
29
+ from mcp_security_framework.schemas.config import RateLimitConfig
30
+ from mcp_security_framework.schemas.models import RateLimitStatus
31
+
32
+
33
+ class TestRateLimitEntry:
34
+ """Test suite for RateLimitEntry class."""
35
+
36
+ def setup_method(self):
37
+ """Set up test fixtures before each test method."""
38
+ self.identifier = "test_identifier"
39
+ self.limit = 10
40
+ self.window_size = 60
41
+ self.entry = RateLimitEntry(self.identifier, self.limit, self.window_size)
42
+
43
+ def test_rate_limit_entry_initialization(self):
44
+ """Test RateLimitEntry initialization."""
45
+ assert self.entry.identifier == self.identifier
46
+ assert self.entry.count == 0
47
+ assert self.entry.limit == self.limit
48
+ assert self.entry.window_size == self.window_size
49
+ assert isinstance(self.entry.window_start, datetime)
50
+
51
+ def test_rate_limit_entry_increment(self):
52
+ """Test request count increment."""
53
+ initial_count = self.entry.count
54
+ new_count = self.entry.increment()
55
+
56
+ assert new_count == initial_count + 1
57
+ assert self.entry.count == new_count
58
+
59
+ def test_rate_limit_entry_is_expired_false(self):
60
+ """Test is_expired returns False for non-expired entry."""
61
+ assert not self.entry.is_expired()
62
+
63
+ def test_rate_limit_entry_is_expired_true(self):
64
+ """Test is_expired returns True for expired entry."""
65
+ # Manually set window start to past
66
+ self.entry.window_start = datetime.now(timezone.utc) - timedelta(
67
+ seconds=self.window_size + 1
68
+ )
69
+ assert self.entry.is_expired()
70
+
71
+ def test_rate_limit_entry_reset_window(self):
72
+ """Test window reset functionality."""
73
+ # Increment count first
74
+ self.entry.increment()
75
+ self.entry.increment()
76
+ assert self.entry.count == 2
77
+
78
+ # Store original window start
79
+ original_start = self.entry.window_start
80
+
81
+ # Reset window
82
+ self.entry.reset_window()
83
+
84
+ assert self.entry.count == 0
85
+ assert self.entry.window_start > original_start
86
+
87
+ def test_rate_limit_entry_get_status(self):
88
+ """Test get_status method."""
89
+ # Increment count
90
+ self.entry.increment()
91
+ self.entry.increment()
92
+
93
+ status = self.entry.get_status()
94
+
95
+ assert isinstance(status, RateLimitStatus)
96
+ assert status.identifier == self.identifier
97
+ assert status.current_count == 2
98
+ assert status.limit == self.limit
99
+ assert status.is_exceeded is False
100
+ assert status.remaining_requests == self.limit - 2
101
+ assert status.window_size_seconds == self.window_size
102
+
103
+ def test_rate_limit_entry_get_status_exceeded(self):
104
+ """Test get_status when limit is exceeded."""
105
+ # Increment beyond limit
106
+ for _ in range(self.limit + 1):
107
+ self.entry.increment()
108
+
109
+ status = self.entry.get_status()
110
+
111
+ assert status.is_exceeded is True
112
+ assert status.remaining_requests == 0
113
+ assert status.current_count > self.limit
114
+
115
+
116
+ class TestRateLimiter:
117
+ """Test suite for RateLimiter class."""
118
+
119
+ def setup_method(self):
120
+ """Set up test fixtures before each test method."""
121
+ self.config = RateLimitConfig(
122
+ enabled=True,
123
+ default_requests_per_minute=10,
124
+ window_size_seconds=60,
125
+ cleanup_interval=300,
126
+ exempt_paths=["/health", "/metrics"],
127
+ exempt_roles=["admin", "monitor"],
128
+ )
129
+ self.rate_limiter = RateLimiter(self.config)
130
+
131
+ def teardown_method(self):
132
+ """Clean up after each test method."""
133
+ if hasattr(self, "rate_limiter"):
134
+ self.rate_limiter.stop_cleanup()
135
+
136
+ def test_rate_limiter_initialization(self):
137
+ """Test RateLimiter initialization."""
138
+ assert self.rate_limiter.config == self.config
139
+ assert self.rate_limiter.config.enabled is True
140
+ assert self.rate_limiter.config.default_requests_per_minute == 10
141
+ assert self.rate_limiter.config.window_size_seconds == 60
142
+
143
+ def test_rate_limiter_initialization_disabled(self):
144
+ """Test RateLimiter initialization when disabled."""
145
+ config = RateLimitConfig(enabled=False)
146
+ rate_limiter = RateLimiter(config)
147
+
148
+ assert rate_limiter.config.enabled is False
149
+ rate_limiter.stop_cleanup()
150
+
151
+ def test_check_rate_limit_basic(self):
152
+ """Test basic rate limit checking."""
153
+ identifier = "test_ip"
154
+
155
+ # First 20 requests should pass (10 * burst_limit = 20)
156
+ for i in range(20):
157
+ assert self.rate_limiter.check_rate_limit(identifier) is True
158
+ self.rate_limiter.increment_request_count(identifier)
159
+
160
+ # 21st request should fail
161
+ assert self.rate_limiter.check_rate_limit(identifier) is False
162
+
163
+ def test_check_rate_limit_custom_limit(self):
164
+ """Test rate limit checking with custom limit."""
165
+ identifier = "test_ip"
166
+ custom_limit = 5
167
+
168
+ # First 5 requests should pass (custom limit overrides burst_limit)
169
+ for i in range(5):
170
+ assert self.rate_limiter.check_rate_limit(identifier, custom_limit) is True
171
+ self.rate_limiter.increment_request_count(identifier, custom_limit)
172
+
173
+ # 6th request should fail
174
+ assert self.rate_limiter.check_rate_limit(identifier, custom_limit) is False
175
+
176
+ def test_check_rate_limit_disabled(self):
177
+ """Test rate limit checking when disabled."""
178
+ config = RateLimitConfig(enabled=False)
179
+ rate_limiter = RateLimiter(config)
180
+
181
+ identifier = "test_ip"
182
+
183
+ # All requests should pass when disabled
184
+ for i in range(20):
185
+ assert rate_limiter.check_rate_limit(identifier) is True
186
+
187
+ rate_limiter.stop_cleanup()
188
+
189
+ def test_check_rate_limit_empty_identifier(self):
190
+ """Test rate limit checking with empty identifier."""
191
+ with pytest.raises(ValueError, match="Identifier cannot be empty"):
192
+ self.rate_limiter.check_rate_limit("")
193
+
194
+ def test_check_rate_limit_invalid_limit(self):
195
+ """Test rate limit checking with invalid limit."""
196
+ with pytest.raises(ValueError, match="Limit must be a positive integer"):
197
+ self.rate_limiter.check_rate_limit("test_ip", 0)
198
+
199
+ with pytest.raises(ValueError, match="Limit must be a positive integer"):
200
+ self.rate_limiter.check_rate_limit("test_ip", -1)
201
+
202
+ def test_increment_request_count_basic(self):
203
+ """Test basic request count increment."""
204
+ identifier = "test_ip"
205
+
206
+ # Increment count
207
+ count = self.rate_limiter.increment_request_count(identifier)
208
+ assert count == 1
209
+
210
+ count = self.rate_limiter.increment_request_count(identifier)
211
+ assert count == 2
212
+
213
+ def test_increment_request_count_custom_limit(self):
214
+ """Test request count increment with custom limit."""
215
+ identifier = "test_ip"
216
+ custom_limit = 5
217
+
218
+ count = self.rate_limiter.increment_request_count(identifier, custom_limit)
219
+ assert count == 1
220
+
221
+ def test_increment_request_count_disabled(self):
222
+ """Test request count increment when disabled."""
223
+ config = RateLimitConfig(enabled=False)
224
+ rate_limiter = RateLimiter(config)
225
+
226
+ identifier = "test_ip"
227
+ count = rate_limiter.increment_request_count(identifier)
228
+ assert count == 0
229
+
230
+ rate_limiter.stop_cleanup()
231
+
232
+ def test_increment_request_count_empty_identifier(self):
233
+ """Test request count increment with empty identifier."""
234
+ with pytest.raises(ValueError, match="Identifier cannot be empty"):
235
+ self.rate_limiter.increment_request_count("")
236
+
237
+ def test_increment_request_count_invalid_limit(self):
238
+ """Test request count increment with invalid limit."""
239
+ with pytest.raises(ValueError, match="Limit must be a positive integer"):
240
+ self.rate_limiter.increment_request_count("test_ip", 0)
241
+
242
+ def test_reset_rate_limit(self):
243
+ """Test rate limit reset functionality."""
244
+ identifier = "test_ip"
245
+
246
+ # Increment count
247
+ self.rate_limiter.increment_request_count(identifier)
248
+ self.rate_limiter.increment_request_count(identifier)
249
+
250
+ # Check that limit is approaching
251
+ assert self.rate_limiter.check_rate_limit(identifier) is True
252
+
253
+ # Reset rate limit
254
+ self.rate_limiter.reset_rate_limit(identifier)
255
+
256
+ # Should be able to make requests again
257
+ for i in range(20): # 10 * burst_limit = 20
258
+ assert self.rate_limiter.check_rate_limit(identifier) is True
259
+ self.rate_limiter.increment_request_count(identifier)
260
+
261
+ def test_reset_rate_limit_empty_identifier(self):
262
+ """Test rate limit reset with empty identifier."""
263
+ with pytest.raises(ValueError, match="Identifier cannot be empty"):
264
+ self.rate_limiter.reset_rate_limit("")
265
+
266
+ def test_get_rate_limit_status_basic(self):
267
+ """Test basic rate limit status retrieval."""
268
+ identifier = "test_ip"
269
+
270
+ # Get initial status
271
+ status = self.rate_limiter.get_rate_limit_status(identifier)
272
+
273
+ assert isinstance(status, RateLimitStatus)
274
+ assert status.identifier == identifier
275
+ assert status.current_count == 0
276
+ assert (
277
+ status.limit
278
+ == self.config.default_requests_per_minute * self.config.burst_limit
279
+ )
280
+ assert status.is_exceeded is False
281
+ assert (
282
+ status.remaining_requests
283
+ == self.config.default_requests_per_minute * self.config.burst_limit
284
+ )
285
+
286
+ def test_get_rate_limit_status_with_increments(self):
287
+ """Test rate limit status after increments."""
288
+ identifier = "test_ip"
289
+
290
+ # Increment count
291
+ self.rate_limiter.increment_request_count(identifier)
292
+ self.rate_limiter.increment_request_count(identifier)
293
+
294
+ status = self.rate_limiter.get_rate_limit_status(identifier)
295
+
296
+ assert status.current_count == 2
297
+ assert status.is_exceeded is False
298
+ assert (
299
+ status.remaining_requests
300
+ == (self.config.default_requests_per_minute * self.config.burst_limit) - 2
301
+ )
302
+
303
+ def test_get_rate_limit_status_exceeded(self):
304
+ """Test rate limit status when exceeded."""
305
+ identifier = "test_ip"
306
+
307
+ # Increment beyond limit
308
+ for _ in range(
309
+ (self.config.default_requests_per_minute * self.config.burst_limit) + 1
310
+ ):
311
+ self.rate_limiter.increment_request_count(identifier)
312
+
313
+ status = self.rate_limiter.get_rate_limit_status(identifier)
314
+
315
+ assert status.is_exceeded is True
316
+ assert status.remaining_requests == 0
317
+
318
+ def test_get_rate_limit_status_disabled(self):
319
+ """Test rate limit status when disabled."""
320
+ config = RateLimitConfig(enabled=False)
321
+ rate_limiter = RateLimiter(config)
322
+
323
+ identifier = "test_ip"
324
+ status = rate_limiter.get_rate_limit_status(identifier)
325
+
326
+ assert status.current_count == 0
327
+ assert status.is_exceeded is False
328
+ assert status.remaining_requests == config.default_requests_per_minute
329
+
330
+ rate_limiter.stop_cleanup()
331
+
332
+ def test_get_rate_limit_status_empty_identifier(self):
333
+ """Test rate limit status with empty identifier."""
334
+ with pytest.raises(ValueError, match="Identifier cannot be empty"):
335
+ self.rate_limiter.get_rate_limit_status("")
336
+
337
+ def test_get_rate_limit_status_invalid_limit(self):
338
+ """Test rate limit status with invalid limit."""
339
+ with pytest.raises(ValueError, match="Limit must be a positive integer"):
340
+ self.rate_limiter.get_rate_limit_status("test_ip", 0)
341
+
342
+ def test_is_exempt_path(self):
343
+ """Test exemption based on path."""
344
+ identifier = "test_ip"
345
+ exempt_path = "/health"
346
+ non_exempt_path = "/api/data"
347
+
348
+ assert self.rate_limiter.is_exempt(identifier, path=exempt_path) is True
349
+ assert self.rate_limiter.is_exempt(identifier, path=non_exempt_path) is False
350
+
351
+ def test_is_exempt_roles(self):
352
+ """Test exemption based on roles."""
353
+ identifier = "test_ip"
354
+ exempt_roles = {"admin"}
355
+ non_exempt_roles = {"user"}
356
+
357
+ assert self.rate_limiter.is_exempt(identifier, roles=exempt_roles) is True
358
+ assert self.rate_limiter.is_exempt(identifier, roles=non_exempt_roles) is False
359
+
360
+ def test_is_exempt_empty_identifier(self):
361
+ """Test exemption with empty identifier."""
362
+ assert self.rate_limiter.is_exempt("", path="/health") is False
363
+ assert self.rate_limiter.is_exempt("", roles={"admin"}) is False
364
+
365
+ def test_cleanup_expired_entries(self):
366
+ """Test cleanup of expired entries."""
367
+ identifier = "test_ip"
368
+
369
+ # Create an entry
370
+ self.rate_limiter.increment_request_count(identifier)
371
+
372
+ # Manually expire the entry
373
+ entry = self.rate_limiter._entries[identifier]
374
+ entry.window_start = datetime.now(timezone.utc) - timedelta(
375
+ seconds=self.config.window_size_seconds + 1
376
+ )
377
+
378
+ # Cleanup should remove expired entry
379
+ cleaned_count = self.rate_limiter.cleanup_expired_entries()
380
+
381
+ assert cleaned_count == 1
382
+ assert identifier not in self.rate_limiter._entries
383
+
384
+ def test_cleanup_expired_entries_disabled(self):
385
+ """Test cleanup when rate limiting is disabled."""
386
+ config = RateLimitConfig(enabled=False)
387
+ rate_limiter = RateLimiter(config)
388
+
389
+ cleaned_count = rate_limiter.cleanup_expired_entries()
390
+ assert cleaned_count == 0
391
+
392
+ rate_limiter.stop_cleanup()
393
+
394
+ def test_get_statistics(self):
395
+ """Test statistics retrieval."""
396
+ identifier = "test_ip"
397
+
398
+ # Create some entries
399
+ self.rate_limiter.increment_request_count(identifier)
400
+ self.rate_limiter.increment_request_count("another_ip")
401
+
402
+ stats = self.rate_limiter.get_statistics()
403
+
404
+ assert stats["enabled"] is True
405
+ assert stats["active_entries"] == 2
406
+ assert (
407
+ stats["default_requests_per_minute"]
408
+ == self.config.default_requests_per_minute
409
+ )
410
+ assert stats["window_size_seconds"] == self.config.window_size_seconds
411
+ assert stats["storage_backend"] == self.config.storage_backend
412
+ assert stats["cleanup_interval"] == self.config.cleanup_interval
413
+
414
+ def test_window_reset_behavior(self):
415
+ """Test window reset behavior."""
416
+ identifier = "test_ip"
417
+
418
+ # Use a short window for testing
419
+ config = RateLimitConfig(
420
+ enabled=True,
421
+ default_requests_per_minute=5,
422
+ burst_limit=1, # Set burst_limit to 1 for this test
423
+ window_size_seconds=1, # 1 second window
424
+ )
425
+ rate_limiter = RateLimiter(config)
426
+
427
+ # Make requests up to limit
428
+ for i in range(5):
429
+ assert rate_limiter.check_rate_limit(identifier) is True
430
+ rate_limiter.increment_request_count(identifier)
431
+
432
+ # Next request should be blocked
433
+ assert rate_limiter.check_rate_limit(identifier) is False
434
+
435
+ # Wait for window to expire
436
+ time.sleep(1.1)
437
+
438
+ # Should be able to make requests again
439
+ assert rate_limiter.check_rate_limit(identifier) is True
440
+
441
+ rate_limiter.stop_cleanup()
442
+
443
+ def test_multiple_identifiers(self):
444
+ """Test rate limiting with multiple identifiers."""
445
+ identifier1 = "ip_1"
446
+ identifier2 = "ip_2"
447
+
448
+ # Both should be able to make requests independently
449
+ for i in range(5):
450
+ assert self.rate_limiter.check_rate_limit(identifier1) is True
451
+ assert self.rate_limiter.check_rate_limit(identifier2) is True
452
+ self.rate_limiter.increment_request_count(identifier1)
453
+ self.rate_limiter.increment_request_count(identifier2)
454
+
455
+ # Both should still have remaining requests
456
+ status1 = self.rate_limiter.get_rate_limit_status(identifier1)
457
+ status2 = self.rate_limiter.get_rate_limit_status(identifier2)
458
+
459
+ assert status1.remaining_requests == 15 # 20 - 5 = 15
460
+ assert status2.remaining_requests == 15 # 20 - 5 = 15
461
+
462
+ def test_thread_safety(self):
463
+ """Test thread safety of rate limiter."""
464
+ import threading
465
+
466
+ identifier = "test_ip"
467
+ results = []
468
+ errors = []
469
+
470
+ def make_requests():
471
+ try:
472
+ for i in range(5):
473
+ result = self.rate_limiter.check_rate_limit(identifier)
474
+ if result:
475
+ self.rate_limiter.increment_request_count(identifier)
476
+ results.append(result)
477
+ except Exception as e:
478
+ errors.append(e)
479
+
480
+ # Create multiple threads
481
+ threads = []
482
+ for _ in range(3):
483
+ thread = threading.Thread(target=make_requests)
484
+ threads.append(thread)
485
+ thread.start()
486
+
487
+ # Wait for all threads to complete
488
+ for thread in threads:
489
+ thread.join()
490
+
491
+ # Should not have any errors
492
+ assert len(errors) == 0
493
+
494
+ # Should have proper rate limiting
495
+ total_requests = sum(1 for r in results if r)
496
+ assert (
497
+ total_requests
498
+ <= self.config.default_requests_per_minute * self.config.burst_limit
499
+ )
500
+
501
+ def test_rate_limit_status_properties(self):
502
+ """Test RateLimitStatus properties."""
503
+ identifier = "test_ip"
504
+
505
+ # Get status
506
+ status = self.rate_limiter.get_rate_limit_status(identifier)
507
+
508
+ # Test seconds_until_reset property
509
+ seconds_until_reset = status.seconds_until_reset
510
+ assert isinstance(seconds_until_reset, int)
511
+ assert seconds_until_reset >= 0
512
+
513
+ # Test utilization_percentage property
514
+ utilization = status.utilization_percentage
515
+ assert isinstance(utilization, float)
516
+ assert utilization == 0.0 # No requests made yet
517
+
518
+ # Make some requests and check utilization
519
+ self.rate_limiter.increment_request_count(identifier)
520
+ self.rate_limiter.increment_request_count(identifier)
521
+
522
+ status = self.rate_limiter.get_rate_limit_status(identifier)
523
+ utilization = status.utilization_percentage
524
+ assert utilization == 10.0 # 2 out of 20 requests = 10%
525
+
526
+ @pytest.mark.slow
527
+ def test_cleanup_thread_functionality(self):
528
+ """Test background cleanup thread functionality."""
529
+ identifier = "test_ip"
530
+
531
+ # Create an entry
532
+ self.rate_limiter.increment_request_count(identifier)
533
+
534
+ # Manually expire the entry
535
+ entry = self.rate_limiter._entries[identifier]
536
+ entry.window_start = datetime.now(timezone.utc) - timedelta(
537
+ seconds=self.config.window_size_seconds + 1
538
+ )
539
+
540
+ # Manually trigger cleanup
541
+ cleaned_count = self.rate_limiter.cleanup_expired_entries()
542
+
543
+ # Entry should be cleaned up
544
+ assert cleaned_count == 1
545
+ assert identifier not in self.rate_limiter._entries
546
+
547
+ def test_rate_limiter_destruction(self):
548
+ """Test rate limiter destruction cleanup."""
549
+ config = RateLimitConfig(enabled=True, cleanup_interval=1)
550
+ rate_limiter = RateLimiter(config)
551
+
552
+ # Verify cleanup thread is running
553
+ assert rate_limiter._cleanup_thread is not None
554
+ assert rate_limiter._cleanup_thread.is_alive()
555
+
556
+ # Destroy rate limiter
557
+ rate_limiter.stop_cleanup()
558
+
559
+ # Thread should be stopped
560
+ assert rate_limiter._stop_cleanup is True
561
+
562
+ def test_rate_limiter_with_burst_limit(self):
563
+ """Test rate limiter with burst limit configuration."""
564
+ config = RateLimitConfig(
565
+ enabled=True,
566
+ default_requests_per_minute=10,
567
+ burst_limit=3,
568
+ window_size_seconds=60,
569
+ )
570
+ rate_limiter = RateLimiter(config)
571
+
572
+ identifier = "test_ip"
573
+
574
+ # Should be able to make burst_limit * default_requests_per_minute requests
575
+ burst_limit_total = config.burst_limit * config.default_requests_per_minute
576
+
577
+ # Check that we can make exactly burst_limit_total requests
578
+ for i in range(burst_limit_total):
579
+ assert rate_limiter.check_rate_limit(identifier) is True
580
+ rate_limiter.increment_request_count(identifier)
581
+
582
+ # Next request should be blocked
583
+ assert rate_limiter.check_rate_limit(identifier) is False
584
+
585
+ rate_limiter.stop_cleanup()
586
+
587
+ def test_rate_limiter_edge_cases(self):
588
+ """Test rate limiter edge cases."""
589
+ # Test with very high limits
590
+ config = RateLimitConfig(
591
+ enabled=True, default_requests_per_minute=10000, window_size_seconds=3600
592
+ )
593
+ rate_limiter = RateLimiter(config)
594
+
595
+ identifier = "test_ip"
596
+
597
+ # Should handle high limits correctly
598
+ for i in range(100):
599
+ assert rate_limiter.check_rate_limit(identifier) is True
600
+ rate_limiter.increment_request_count(identifier)
601
+
602
+ status = rate_limiter.get_rate_limit_status(identifier)
603
+ assert status.current_count == 100
604
+ assert status.remaining_requests == 19900 # 20000 - 100 = 19900
605
+
606
+ rate_limiter.stop_cleanup()
607
+
608
+ # Test with very low limits
609
+ config = RateLimitConfig(
610
+ enabled=True,
611
+ default_requests_per_minute=1,
612
+ burst_limit=1, # Set burst_limit to 1 for this test
613
+ window_size_seconds=1,
614
+ )
615
+ rate_limiter = RateLimiter(config)
616
+
617
+ identifier = "test_ip"
618
+
619
+ # First request should pass
620
+ assert rate_limiter.check_rate_limit(identifier) is True
621
+ rate_limiter.increment_request_count(identifier)
622
+
623
+ # Second request should fail
624
+ assert rate_limiter.check_rate_limit(identifier) is False
625
+
626
+ rate_limiter.stop_cleanup()