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,1054 @@
|
|
1
|
+
"""
|
2
|
+
Response Models Test Module
|
3
|
+
|
4
|
+
This module provides comprehensive unit tests for all response models
|
5
|
+
in the MCP Security Framework. It tests validation, factory methods,
|
6
|
+
and edge cases for all response classes.
|
7
|
+
|
8
|
+
Test Classes:
|
9
|
+
TestSecurityResponse: Tests for base security response model
|
10
|
+
TestErrorResponse: Tests for error response model
|
11
|
+
TestSuccessResponse: Tests for success response model
|
12
|
+
TestValidationResponse: Tests for validation response model
|
13
|
+
TestAuthResponse: Tests for authentication response model
|
14
|
+
TestCertificateResponse: Tests for certificate response model
|
15
|
+
TestPermissionResponse: Tests for permission response model
|
16
|
+
TestRateLimitResponse: Tests for rate limiting response 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
|
+
CertificateInfo,
|
33
|
+
CertificateType,
|
34
|
+
RateLimitStatus,
|
35
|
+
ValidationResult,
|
36
|
+
ValidationStatus,
|
37
|
+
)
|
38
|
+
from mcp_security_framework.schemas.responses import (
|
39
|
+
AuthResponse,
|
40
|
+
CertificateResponse,
|
41
|
+
ErrorCode,
|
42
|
+
ErrorResponse,
|
43
|
+
PermissionResponse,
|
44
|
+
RateLimitResponse,
|
45
|
+
ResponseStatus,
|
46
|
+
SecurityResponse,
|
47
|
+
SuccessResponse,
|
48
|
+
ValidationResponse,
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
class TestSecurityResponse:
|
53
|
+
"""Test suite for SecurityResponse class."""
|
54
|
+
|
55
|
+
def test_security_response_basic(self):
|
56
|
+
"""Test SecurityResponse with basic information."""
|
57
|
+
response = SecurityResponse(
|
58
|
+
status=ResponseStatus.SUCCESS,
|
59
|
+
message="Operation completed successfully",
|
60
|
+
data={"key": "value"},
|
61
|
+
)
|
62
|
+
|
63
|
+
assert response.status == ResponseStatus.SUCCESS
|
64
|
+
assert response.message == "Operation completed successfully"
|
65
|
+
assert response.data == {"key": "value"}
|
66
|
+
assert response.timestamp is not None
|
67
|
+
assert response.request_id is None
|
68
|
+
assert response.version == "1.0.0"
|
69
|
+
assert response.metadata == {}
|
70
|
+
|
71
|
+
def test_security_response_with_metadata(self):
|
72
|
+
"""Test SecurityResponse with metadata."""
|
73
|
+
response = SecurityResponse(
|
74
|
+
status=ResponseStatus.SUCCESS,
|
75
|
+
message="Operation completed successfully",
|
76
|
+
data={"key": "value"},
|
77
|
+
request_id="req-123",
|
78
|
+
metadata={"user_id": "12345", "operation": "create"},
|
79
|
+
)
|
80
|
+
|
81
|
+
assert response.request_id == "req-123"
|
82
|
+
assert response.metadata == {"user_id": "12345", "operation": "create"}
|
83
|
+
|
84
|
+
def test_security_response_message_validation(self):
|
85
|
+
"""Test SecurityResponse message validation."""
|
86
|
+
# Valid message
|
87
|
+
response = SecurityResponse(
|
88
|
+
status=ResponseStatus.SUCCESS, message="Valid message"
|
89
|
+
)
|
90
|
+
assert response.message == "Valid message"
|
91
|
+
|
92
|
+
# Message with whitespace
|
93
|
+
response = SecurityResponse(
|
94
|
+
status=ResponseStatus.SUCCESS, message=" Valid message "
|
95
|
+
)
|
96
|
+
assert response.message == "Valid message"
|
97
|
+
|
98
|
+
# Empty message
|
99
|
+
with pytest.raises(ValidationError) as exc_info:
|
100
|
+
SecurityResponse(status=ResponseStatus.SUCCESS, message="")
|
101
|
+
|
102
|
+
assert "Response message cannot be empty" in str(exc_info.value)
|
103
|
+
|
104
|
+
def test_security_response_properties(self):
|
105
|
+
"""Test SecurityResponse properties."""
|
106
|
+
# Success response
|
107
|
+
response = SecurityResponse(status=ResponseStatus.SUCCESS, message="Success")
|
108
|
+
|
109
|
+
assert response.is_success is True
|
110
|
+
assert response.is_error is False
|
111
|
+
|
112
|
+
# Error response
|
113
|
+
response = SecurityResponse(status=ResponseStatus.ERROR, message="Error")
|
114
|
+
|
115
|
+
assert response.is_success is False
|
116
|
+
assert response.is_error is True
|
117
|
+
|
118
|
+
|
119
|
+
class TestErrorResponse:
|
120
|
+
"""Test suite for ErrorResponse class."""
|
121
|
+
|
122
|
+
def test_error_response_basic(self):
|
123
|
+
"""Test ErrorResponse with basic information."""
|
124
|
+
error_response = ErrorResponse(
|
125
|
+
status=ResponseStatus.ERROR,
|
126
|
+
message="Authentication failed",
|
127
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
128
|
+
http_status_code=401,
|
129
|
+
details="Invalid API key provided",
|
130
|
+
)
|
131
|
+
|
132
|
+
assert error_response.status == ResponseStatus.ERROR
|
133
|
+
assert error_response.message == "Authentication failed"
|
134
|
+
assert error_response.error_code == ErrorCode.AUTHENTICATION_FAILED
|
135
|
+
assert error_response.http_status_code == 401
|
136
|
+
assert error_response.details == "Invalid API key provided"
|
137
|
+
assert error_response.error_type == "SecurityError"
|
138
|
+
assert error_response.field_errors == {}
|
139
|
+
assert error_response.stack_trace is None
|
140
|
+
assert error_response.retry_after is None
|
141
|
+
|
142
|
+
def test_error_response_complete(self):
|
143
|
+
"""Test ErrorResponse with complete information."""
|
144
|
+
error_response = ErrorResponse(
|
145
|
+
status=ResponseStatus.ERROR,
|
146
|
+
message="Validation failed",
|
147
|
+
error_code=ErrorCode.VALIDATION_ERROR,
|
148
|
+
http_status_code=400,
|
149
|
+
details="Multiple validation errors occurred",
|
150
|
+
field_errors={"username": ["Required field"], "email": ["Invalid format"]},
|
151
|
+
stack_trace="Traceback (most recent call last):...",
|
152
|
+
retry_after=60,
|
153
|
+
error_type="ValidationError",
|
154
|
+
)
|
155
|
+
|
156
|
+
assert error_response.field_errors == {
|
157
|
+
"username": ["Required field"],
|
158
|
+
"email": ["Invalid format"],
|
159
|
+
}
|
160
|
+
assert error_response.stack_trace == "Traceback (most recent call last):..."
|
161
|
+
assert error_response.retry_after == 60
|
162
|
+
assert error_response.error_type == "ValidationError"
|
163
|
+
|
164
|
+
def test_error_response_status_validation(self):
|
165
|
+
"""Test ErrorResponse status validation."""
|
166
|
+
with pytest.raises(ValidationError) as exc_info:
|
167
|
+
ErrorResponse(
|
168
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
169
|
+
message="Error message",
|
170
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
171
|
+
http_status_code=401,
|
172
|
+
)
|
173
|
+
|
174
|
+
assert "Error responses must have ERROR status" in str(exc_info.value)
|
175
|
+
|
176
|
+
def test_error_response_http_status_code_validation(self):
|
177
|
+
"""Test ErrorResponse HTTP status code validation."""
|
178
|
+
# Valid range
|
179
|
+
error_response = ErrorResponse(
|
180
|
+
status=ResponseStatus.ERROR,
|
181
|
+
message="Error",
|
182
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
183
|
+
http_status_code=401,
|
184
|
+
)
|
185
|
+
assert error_response.http_status_code == 401
|
186
|
+
|
187
|
+
# Invalid range - too low
|
188
|
+
with pytest.raises(ValidationError) as exc_info:
|
189
|
+
ErrorResponse(
|
190
|
+
status=ResponseStatus.ERROR,
|
191
|
+
message="Error",
|
192
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
193
|
+
http_status_code=200, # Should be 4xx or 5xx
|
194
|
+
)
|
195
|
+
|
196
|
+
assert "Input should be greater than or equal to 400" in str(exc_info.value)
|
197
|
+
|
198
|
+
# Invalid range - too high
|
199
|
+
with pytest.raises(ValidationError) as exc_info:
|
200
|
+
ErrorResponse(
|
201
|
+
status=ResponseStatus.ERROR,
|
202
|
+
message="Error",
|
203
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
204
|
+
http_status_code=600, # Should be 4xx or 5xx
|
205
|
+
)
|
206
|
+
|
207
|
+
assert "Input should be less than or equal to 599" in str(exc_info.value)
|
208
|
+
|
209
|
+
def test_error_response_consistency_validation(self):
|
210
|
+
"""Test ErrorResponse consistency validation."""
|
211
|
+
# Authentication failed should have 401
|
212
|
+
with pytest.raises(ValidationError) as exc_info:
|
213
|
+
ErrorResponse(
|
214
|
+
status=ResponseStatus.ERROR,
|
215
|
+
message="Authentication failed",
|
216
|
+
error_code=ErrorCode.AUTHENTICATION_FAILED,
|
217
|
+
http_status_code=403, # Should be 401
|
218
|
+
)
|
219
|
+
|
220
|
+
assert "Authentication failed should have HTTP status 401" in str(
|
221
|
+
exc_info.value
|
222
|
+
)
|
223
|
+
|
224
|
+
# Insufficient permissions should have 403
|
225
|
+
with pytest.raises(ValidationError) as exc_info:
|
226
|
+
ErrorResponse(
|
227
|
+
status=ResponseStatus.ERROR,
|
228
|
+
message="Insufficient permissions",
|
229
|
+
error_code=ErrorCode.INSUFFICIENT_PERMISSIONS,
|
230
|
+
http_status_code=401, # Should be 403
|
231
|
+
)
|
232
|
+
|
233
|
+
assert "Insufficient permissions should have HTTP status 403" in str(
|
234
|
+
exc_info.value
|
235
|
+
)
|
236
|
+
|
237
|
+
# Rate limit exceeded should have 429
|
238
|
+
with pytest.raises(ValidationError) as exc_info:
|
239
|
+
ErrorResponse(
|
240
|
+
status=ResponseStatus.ERROR,
|
241
|
+
message="Rate limit exceeded",
|
242
|
+
error_code=ErrorCode.RATE_LIMIT_EXCEEDED,
|
243
|
+
http_status_code=400, # Should be 429
|
244
|
+
)
|
245
|
+
|
246
|
+
assert "Rate limit exceeded should have HTTP status 429" in str(exc_info.value)
|
247
|
+
|
248
|
+
def test_error_response_factory_methods(self):
|
249
|
+
"""Test ErrorResponse factory methods."""
|
250
|
+
# Authentication error
|
251
|
+
auth_error = ErrorResponse.create_authentication_error(
|
252
|
+
"Invalid credentials", "API key not found"
|
253
|
+
)
|
254
|
+
|
255
|
+
assert auth_error.status == ResponseStatus.ERROR
|
256
|
+
assert auth_error.message == "Invalid credentials"
|
257
|
+
assert auth_error.error_code == ErrorCode.AUTHENTICATION_FAILED
|
258
|
+
assert auth_error.http_status_code == 401
|
259
|
+
assert auth_error.details == "API key not found"
|
260
|
+
assert auth_error.error_type == "AuthenticationError"
|
261
|
+
|
262
|
+
# Permission error
|
263
|
+
perm_error = ErrorResponse.create_permission_error(
|
264
|
+
"Access denied", "User lacks required permissions"
|
265
|
+
)
|
266
|
+
|
267
|
+
assert perm_error.status == ResponseStatus.ERROR
|
268
|
+
assert perm_error.message == "Access denied"
|
269
|
+
assert perm_error.error_code == ErrorCode.INSUFFICIENT_PERMISSIONS
|
270
|
+
assert perm_error.http_status_code == 403
|
271
|
+
assert perm_error.details == "User lacks required permissions"
|
272
|
+
assert perm_error.error_type == "PermissionError"
|
273
|
+
|
274
|
+
# Rate limit error
|
275
|
+
rate_error = ErrorResponse.create_rate_limit_error("Too many requests", 60)
|
276
|
+
|
277
|
+
assert rate_error.status == ResponseStatus.ERROR
|
278
|
+
assert rate_error.message == "Too many requests"
|
279
|
+
assert rate_error.error_code == ErrorCode.RATE_LIMIT_EXCEEDED
|
280
|
+
assert rate_error.http_status_code == 429
|
281
|
+
assert rate_error.retry_after == 60
|
282
|
+
assert rate_error.error_type == "RateLimitError"
|
283
|
+
|
284
|
+
# Validation error
|
285
|
+
field_errors = {"username": ["Required"], "email": ["Invalid format"]}
|
286
|
+
val_error = ErrorResponse.create_validation_error(
|
287
|
+
"Validation failed", field_errors
|
288
|
+
)
|
289
|
+
|
290
|
+
assert val_error.status == ResponseStatus.ERROR
|
291
|
+
assert val_error.message == "Validation failed"
|
292
|
+
assert val_error.error_code == ErrorCode.VALIDATION_ERROR
|
293
|
+
assert val_error.http_status_code == 400
|
294
|
+
assert val_error.field_errors == field_errors
|
295
|
+
assert val_error.error_type == "ValidationError"
|
296
|
+
|
297
|
+
|
298
|
+
class TestSuccessResponse:
|
299
|
+
"""Test suite for SuccessResponse class."""
|
300
|
+
|
301
|
+
def test_success_response_basic(self):
|
302
|
+
"""Test SuccessResponse with basic information."""
|
303
|
+
success_response = SuccessResponse(
|
304
|
+
status=ResponseStatus.SUCCESS,
|
305
|
+
message="Operation completed successfully",
|
306
|
+
data={"user_id": "12345", "status": "active"},
|
307
|
+
)
|
308
|
+
|
309
|
+
assert success_response.status == ResponseStatus.SUCCESS
|
310
|
+
assert success_response.message == "Operation completed successfully"
|
311
|
+
assert success_response.data == {"user_id": "12345", "status": "active"}
|
312
|
+
assert success_response.total_count is None
|
313
|
+
assert success_response.page is None
|
314
|
+
assert success_response.page_size is None
|
315
|
+
assert success_response.has_more is None
|
316
|
+
|
317
|
+
def test_success_response_with_pagination(self):
|
318
|
+
"""Test SuccessResponse with pagination information."""
|
319
|
+
success_response = SuccessResponse(
|
320
|
+
status=ResponseStatus.SUCCESS,
|
321
|
+
message="Users retrieved successfully",
|
322
|
+
data=[{"id": "1"}, {"id": "2"}],
|
323
|
+
total_count=100,
|
324
|
+
page=1,
|
325
|
+
page_size=10,
|
326
|
+
has_more=True,
|
327
|
+
)
|
328
|
+
|
329
|
+
assert success_response.total_count == 100
|
330
|
+
assert success_response.page == 1
|
331
|
+
assert success_response.page_size == 10
|
332
|
+
assert success_response.has_more is True
|
333
|
+
|
334
|
+
def test_success_response_status_validation(self):
|
335
|
+
"""Test SuccessResponse status validation."""
|
336
|
+
with pytest.raises(ValidationError) as exc_info:
|
337
|
+
SuccessResponse(
|
338
|
+
status=ResponseStatus.ERROR, # Should be SUCCESS
|
339
|
+
message="Success",
|
340
|
+
data={"key": "value"},
|
341
|
+
)
|
342
|
+
|
343
|
+
assert "Success responses must have SUCCESS status" in str(exc_info.value)
|
344
|
+
|
345
|
+
def test_success_response_data_validation(self):
|
346
|
+
"""Test SuccessResponse data validation."""
|
347
|
+
with pytest.raises(ValidationError) as exc_info:
|
348
|
+
SuccessResponse(
|
349
|
+
status=ResponseStatus.SUCCESS,
|
350
|
+
message="Success",
|
351
|
+
data=None, # Should not be None
|
352
|
+
)
|
353
|
+
|
354
|
+
assert "Success responses must have data" in str(exc_info.value)
|
355
|
+
|
356
|
+
def test_success_response_pagination_validation(self):
|
357
|
+
"""Test SuccessResponse pagination validation."""
|
358
|
+
# Valid pagination
|
359
|
+
success_response = SuccessResponse(
|
360
|
+
status=ResponseStatus.SUCCESS,
|
361
|
+
message="Success",
|
362
|
+
data=[],
|
363
|
+
total_count=0,
|
364
|
+
page=1,
|
365
|
+
page_size=10,
|
366
|
+
)
|
367
|
+
assert success_response.total_count == 0
|
368
|
+
assert success_response.page == 1
|
369
|
+
assert success_response.page_size == 10
|
370
|
+
|
371
|
+
# Invalid total_count
|
372
|
+
with pytest.raises(ValidationError):
|
373
|
+
SuccessResponse(
|
374
|
+
message="Success", data=[], total_count=-1
|
375
|
+
) # Should be >= 0
|
376
|
+
|
377
|
+
# Invalid page
|
378
|
+
with pytest.raises(ValidationError):
|
379
|
+
SuccessResponse(message="Success", data=[], page=0) # Should be >= 1
|
380
|
+
|
381
|
+
# Invalid page_size
|
382
|
+
with pytest.raises(ValidationError):
|
383
|
+
SuccessResponse(message="Success", data=[], page_size=0) # Should be >= 1
|
384
|
+
|
385
|
+
def test_success_response_factory_method(self):
|
386
|
+
"""Test SuccessResponse factory method."""
|
387
|
+
data = {"user_id": "12345", "name": "John Doe"}
|
388
|
+
success_response = SuccessResponse.create_success(
|
389
|
+
data=data, message="User created successfully"
|
390
|
+
)
|
391
|
+
|
392
|
+
assert success_response.status == ResponseStatus.SUCCESS
|
393
|
+
assert success_response.message == "User created successfully"
|
394
|
+
assert success_response.data == data
|
395
|
+
|
396
|
+
# Default message
|
397
|
+
success_response = SuccessResponse.create_success(data=data)
|
398
|
+
assert success_response.message == "Operation completed successfully"
|
399
|
+
|
400
|
+
|
401
|
+
class TestValidationResponse:
|
402
|
+
"""Test suite for ValidationResponse class."""
|
403
|
+
|
404
|
+
def test_validation_response_success(self):
|
405
|
+
"""Test ValidationResponse with successful validation."""
|
406
|
+
validation_result = ValidationResult(
|
407
|
+
is_valid=True,
|
408
|
+
status=ValidationStatus.VALID,
|
409
|
+
field_name="username",
|
410
|
+
value="testuser",
|
411
|
+
)
|
412
|
+
|
413
|
+
validation_response = ValidationResponse(
|
414
|
+
status=ResponseStatus.SUCCESS,
|
415
|
+
message="Validation completed successfully",
|
416
|
+
validation_result=validation_result,
|
417
|
+
)
|
418
|
+
|
419
|
+
assert validation_response.status == ResponseStatus.SUCCESS
|
420
|
+
assert validation_response.message == "Validation completed successfully"
|
421
|
+
assert validation_response.validation_result == validation_result
|
422
|
+
assert validation_response.field_errors == {}
|
423
|
+
assert validation_response.warnings == []
|
424
|
+
assert validation_response.suggestions == []
|
425
|
+
|
426
|
+
def test_validation_response_error(self):
|
427
|
+
"""Test ValidationResponse with validation error."""
|
428
|
+
validation_result = ValidationResult(
|
429
|
+
is_valid=False,
|
430
|
+
status=ValidationStatus.INVALID,
|
431
|
+
field_name="password",
|
432
|
+
value="",
|
433
|
+
error_code=400,
|
434
|
+
error_message="Password cannot be empty",
|
435
|
+
)
|
436
|
+
|
437
|
+
field_errors = {"password": ["Required field"], "email": ["Invalid format"]}
|
438
|
+
validation_response = ValidationResponse(
|
439
|
+
status=ResponseStatus.ERROR,
|
440
|
+
message="Validation failed",
|
441
|
+
validation_result=validation_result,
|
442
|
+
field_errors=field_errors,
|
443
|
+
warnings=["Password is too weak"],
|
444
|
+
suggestions=["Use at least 8 characters"],
|
445
|
+
)
|
446
|
+
|
447
|
+
assert validation_response.status == ResponseStatus.ERROR
|
448
|
+
assert validation_response.message == "Validation failed"
|
449
|
+
assert validation_response.validation_result == validation_result
|
450
|
+
assert validation_response.field_errors == field_errors
|
451
|
+
assert validation_response.warnings == ["Password is too weak"]
|
452
|
+
assert validation_response.suggestions == ["Use at least 8 characters"]
|
453
|
+
|
454
|
+
def test_validation_response_status_validation(self):
|
455
|
+
"""Test ValidationResponse status validation."""
|
456
|
+
# Valid validation should have SUCCESS status
|
457
|
+
validation_result = ValidationResult(
|
458
|
+
is_valid=True, status=ValidationStatus.VALID
|
459
|
+
)
|
460
|
+
|
461
|
+
with pytest.raises(ValidationError) as exc_info:
|
462
|
+
ValidationResponse(
|
463
|
+
status=ResponseStatus.ERROR, # Should be SUCCESS
|
464
|
+
message="Validation failed",
|
465
|
+
validation_result=validation_result,
|
466
|
+
)
|
467
|
+
|
468
|
+
assert "Valid validation must have SUCCESS status" in str(exc_info.value)
|
469
|
+
|
470
|
+
# Invalid validation should have ERROR status
|
471
|
+
validation_result = ValidationResult(
|
472
|
+
is_valid=False, status=ValidationStatus.INVALID
|
473
|
+
)
|
474
|
+
|
475
|
+
with pytest.raises(ValidationError) as exc_info:
|
476
|
+
ValidationResponse(
|
477
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
478
|
+
message="Validation succeeded",
|
479
|
+
validation_result=validation_result,
|
480
|
+
)
|
481
|
+
|
482
|
+
assert "Invalid validation must have ERROR status" in str(exc_info.value)
|
483
|
+
|
484
|
+
def test_validation_response_factory_methods(self):
|
485
|
+
"""Test ValidationResponse factory methods."""
|
486
|
+
# Success validation
|
487
|
+
validation_result = ValidationResult(
|
488
|
+
is_valid=True,
|
489
|
+
status=ValidationStatus.VALID,
|
490
|
+
field_name="username",
|
491
|
+
value="testuser",
|
492
|
+
)
|
493
|
+
|
494
|
+
success_response = ValidationResponse.create_validation_success(
|
495
|
+
validation_result
|
496
|
+
)
|
497
|
+
|
498
|
+
assert success_response.status == ResponseStatus.SUCCESS
|
499
|
+
assert success_response.message == "Validation completed successfully"
|
500
|
+
assert success_response.validation_result == validation_result
|
501
|
+
|
502
|
+
# Error validation
|
503
|
+
validation_result = ValidationResult(
|
504
|
+
is_valid=False,
|
505
|
+
status=ValidationStatus.INVALID,
|
506
|
+
field_name="password",
|
507
|
+
value="",
|
508
|
+
error_code=400,
|
509
|
+
error_message="Password cannot be empty",
|
510
|
+
)
|
511
|
+
|
512
|
+
field_errors = {"password": ["Required field"]}
|
513
|
+
error_response = ValidationResponse.create_validation_error(
|
514
|
+
validation_result, field_errors
|
515
|
+
)
|
516
|
+
|
517
|
+
assert error_response.status == ResponseStatus.ERROR
|
518
|
+
assert error_response.message == "Validation failed"
|
519
|
+
assert error_response.validation_result == validation_result
|
520
|
+
assert error_response.field_errors == field_errors
|
521
|
+
|
522
|
+
|
523
|
+
class TestAuthResponse:
|
524
|
+
"""Test suite for AuthResponse class."""
|
525
|
+
|
526
|
+
def test_auth_response_success(self):
|
527
|
+
"""Test AuthResponse with successful authentication."""
|
528
|
+
auth_result = AuthResult(
|
529
|
+
is_valid=True,
|
530
|
+
status=AuthStatus.SUCCESS,
|
531
|
+
username="testuser",
|
532
|
+
roles=["user", "admin"],
|
533
|
+
permissions={"read", "write"},
|
534
|
+
auth_method=AuthMethod.API_KEY,
|
535
|
+
)
|
536
|
+
|
537
|
+
auth_response = AuthResponse(
|
538
|
+
status=ResponseStatus.SUCCESS,
|
539
|
+
message="Authentication successful",
|
540
|
+
auth_result=auth_result,
|
541
|
+
user_info={"email": "test@example.com"},
|
542
|
+
session_info={"session_id": "sess-123"},
|
543
|
+
token_info={"expires_in": 3600},
|
544
|
+
)
|
545
|
+
|
546
|
+
assert auth_response.status == ResponseStatus.SUCCESS
|
547
|
+
assert auth_response.message == "Authentication successful"
|
548
|
+
assert auth_response.auth_result == auth_result
|
549
|
+
assert auth_response.user_info == {"email": "test@example.com"}
|
550
|
+
assert auth_response.session_info == {"session_id": "sess-123"}
|
551
|
+
assert auth_response.token_info == {"expires_in": 3600}
|
552
|
+
|
553
|
+
def test_auth_response_error(self):
|
554
|
+
"""Test AuthResponse with authentication error."""
|
555
|
+
auth_result = AuthResult(
|
556
|
+
is_valid=False,
|
557
|
+
status=AuthStatus.FAILED,
|
558
|
+
error_code=401,
|
559
|
+
error_message="Invalid credentials",
|
560
|
+
)
|
561
|
+
|
562
|
+
auth_response = AuthResponse(
|
563
|
+
status=ResponseStatus.ERROR,
|
564
|
+
message="Authentication failed",
|
565
|
+
auth_result=auth_result,
|
566
|
+
)
|
567
|
+
|
568
|
+
assert auth_response.status == ResponseStatus.ERROR
|
569
|
+
assert auth_response.message == "Authentication failed"
|
570
|
+
assert auth_response.auth_result == auth_result
|
571
|
+
assert auth_response.user_info == {}
|
572
|
+
assert auth_response.session_info == {}
|
573
|
+
assert auth_response.token_info == {}
|
574
|
+
|
575
|
+
def test_auth_response_status_validation(self):
|
576
|
+
"""Test AuthResponse status validation."""
|
577
|
+
# Successful auth should have SUCCESS status
|
578
|
+
auth_result = AuthResult(is_valid=True, status=AuthStatus.SUCCESS)
|
579
|
+
|
580
|
+
with pytest.raises(ValidationError) as exc_info:
|
581
|
+
AuthResponse(
|
582
|
+
status=ResponseStatus.ERROR, # Should be SUCCESS
|
583
|
+
message="Authentication failed",
|
584
|
+
auth_result=auth_result,
|
585
|
+
)
|
586
|
+
|
587
|
+
assert "Successful authentication must have SUCCESS status" in str(
|
588
|
+
exc_info.value
|
589
|
+
)
|
590
|
+
|
591
|
+
# Failed auth should have ERROR status
|
592
|
+
auth_result = AuthResult(is_valid=False, status=AuthStatus.FAILED)
|
593
|
+
|
594
|
+
with pytest.raises(ValidationError) as exc_info:
|
595
|
+
AuthResponse(
|
596
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
597
|
+
message="Authentication succeeded",
|
598
|
+
auth_result=auth_result,
|
599
|
+
)
|
600
|
+
|
601
|
+
assert "Failed authentication must have ERROR status" in str(exc_info.value)
|
602
|
+
|
603
|
+
def test_auth_response_factory_methods(self):
|
604
|
+
"""Test AuthResponse factory methods."""
|
605
|
+
# Success authentication
|
606
|
+
auth_result = AuthResult(
|
607
|
+
is_valid=True,
|
608
|
+
status=AuthStatus.SUCCESS,
|
609
|
+
username="testuser",
|
610
|
+
roles=["user"],
|
611
|
+
permissions={"read"},
|
612
|
+
)
|
613
|
+
|
614
|
+
user_info = {"email": "test@example.com"}
|
615
|
+
success_response = AuthResponse.create_auth_success(auth_result, user_info)
|
616
|
+
|
617
|
+
assert success_response.status == ResponseStatus.SUCCESS
|
618
|
+
assert success_response.message == "Authentication successful"
|
619
|
+
assert success_response.auth_result == auth_result
|
620
|
+
assert success_response.user_info == user_info
|
621
|
+
|
622
|
+
# Error authentication
|
623
|
+
auth_result = AuthResult(
|
624
|
+
is_valid=False,
|
625
|
+
status=AuthStatus.FAILED,
|
626
|
+
error_code=401,
|
627
|
+
error_message="Invalid credentials",
|
628
|
+
)
|
629
|
+
|
630
|
+
error_response = AuthResponse.create_auth_error(auth_result)
|
631
|
+
|
632
|
+
assert error_response.status == ResponseStatus.ERROR
|
633
|
+
assert error_response.message == "Invalid credentials"
|
634
|
+
assert error_response.auth_result == auth_result
|
635
|
+
|
636
|
+
|
637
|
+
class TestCertificateResponse:
|
638
|
+
"""Test suite for CertificateResponse class."""
|
639
|
+
|
640
|
+
def test_certificate_response_success(self):
|
641
|
+
"""Test CertificateResponse with successful certificate info."""
|
642
|
+
cert_info = CertificateInfo(
|
643
|
+
subject={"CN": "test.example.com"},
|
644
|
+
issuer={"CN": "Test CA"},
|
645
|
+
serial_number="123456789",
|
646
|
+
not_before=datetime.now(timezone.utc),
|
647
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
648
|
+
certificate_type=CertificateType.SERVER,
|
649
|
+
key_size=2048,
|
650
|
+
signature_algorithm="sha256WithRSAEncryption",
|
651
|
+
)
|
652
|
+
|
653
|
+
cert_response = CertificateResponse(
|
654
|
+
status=ResponseStatus.SUCCESS,
|
655
|
+
message="Certificate information retrieved successfully",
|
656
|
+
certificate_info=cert_info,
|
657
|
+
chain_info={"length": 2},
|
658
|
+
expiry_info={"days_remaining": 300},
|
659
|
+
)
|
660
|
+
|
661
|
+
assert cert_response.status == ResponseStatus.SUCCESS
|
662
|
+
assert cert_response.message == "Certificate information retrieved successfully"
|
663
|
+
assert cert_response.certificate_info == cert_info
|
664
|
+
assert cert_response.validation_result is None
|
665
|
+
assert cert_response.chain_info == {"length": 2}
|
666
|
+
assert cert_response.expiry_info == {"days_remaining": 300}
|
667
|
+
|
668
|
+
def test_certificate_response_with_validation(self):
|
669
|
+
"""Test CertificateResponse with validation result."""
|
670
|
+
cert_info = CertificateInfo(
|
671
|
+
subject={"CN": "test.example.com"},
|
672
|
+
issuer={"CN": "Test CA"},
|
673
|
+
serial_number="123456789",
|
674
|
+
not_before=datetime.now(timezone.utc),
|
675
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
676
|
+
certificate_type=CertificateType.SERVER,
|
677
|
+
key_size=2048,
|
678
|
+
signature_algorithm="sha256WithRSAEncryption",
|
679
|
+
)
|
680
|
+
|
681
|
+
validation_result = ValidationResult(
|
682
|
+
is_valid=True,
|
683
|
+
status=ValidationStatus.VALID,
|
684
|
+
field_name="certificate",
|
685
|
+
value=cert_info,
|
686
|
+
)
|
687
|
+
|
688
|
+
cert_response = CertificateResponse(
|
689
|
+
status=ResponseStatus.SUCCESS,
|
690
|
+
message="Certificate validated successfully",
|
691
|
+
certificate_info=cert_info,
|
692
|
+
validation_result=validation_result,
|
693
|
+
)
|
694
|
+
|
695
|
+
assert cert_response.validation_result == validation_result
|
696
|
+
|
697
|
+
def test_certificate_response_validation_consistency(self):
|
698
|
+
"""Test CertificateResponse validation consistency."""
|
699
|
+
cert_info = CertificateInfo(
|
700
|
+
subject={"CN": "test.example.com"},
|
701
|
+
issuer={"CN": "Test CA"},
|
702
|
+
serial_number="123456789",
|
703
|
+
not_before=datetime.now(timezone.utc),
|
704
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
705
|
+
certificate_type=CertificateType.SERVER,
|
706
|
+
key_size=2048,
|
707
|
+
signature_algorithm="sha256WithRSAEncryption",
|
708
|
+
)
|
709
|
+
|
710
|
+
validation_result = ValidationResult(
|
711
|
+
is_valid=False,
|
712
|
+
status=ValidationStatus.INVALID,
|
713
|
+
field_name="certificate",
|
714
|
+
value=cert_info,
|
715
|
+
)
|
716
|
+
|
717
|
+
# Invalid validation cannot have SUCCESS status
|
718
|
+
with pytest.raises(ValidationError) as exc_info:
|
719
|
+
CertificateResponse(
|
720
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
721
|
+
message="Certificate validated successfully",
|
722
|
+
certificate_info=cert_info,
|
723
|
+
validation_result=validation_result,
|
724
|
+
)
|
725
|
+
|
726
|
+
assert "Invalid certificate validation cannot have SUCCESS status" in str(
|
727
|
+
exc_info.value
|
728
|
+
)
|
729
|
+
|
730
|
+
def test_certificate_response_factory_method(self):
|
731
|
+
"""Test CertificateResponse factory method."""
|
732
|
+
cert_info = CertificateInfo(
|
733
|
+
subject={"CN": "test.example.com"},
|
734
|
+
issuer={"CN": "Test CA"},
|
735
|
+
serial_number="123456789",
|
736
|
+
not_before=datetime.now(timezone.utc),
|
737
|
+
not_after=datetime.now(timezone.utc) + timedelta(days=365),
|
738
|
+
certificate_type=CertificateType.SERVER,
|
739
|
+
key_size=2048,
|
740
|
+
signature_algorithm="sha256WithRSAEncryption",
|
741
|
+
)
|
742
|
+
|
743
|
+
validation_result = ValidationResult(
|
744
|
+
is_valid=True,
|
745
|
+
status=ValidationStatus.VALID,
|
746
|
+
field_name="certificate",
|
747
|
+
value=cert_info,
|
748
|
+
)
|
749
|
+
|
750
|
+
success_response = CertificateResponse.create_certificate_success(
|
751
|
+
cert_info, validation_result
|
752
|
+
)
|
753
|
+
|
754
|
+
assert success_response.status == ResponseStatus.SUCCESS
|
755
|
+
assert (
|
756
|
+
success_response.message == "Certificate information retrieved successfully"
|
757
|
+
)
|
758
|
+
assert success_response.certificate_info == cert_info
|
759
|
+
assert success_response.validation_result == validation_result
|
760
|
+
|
761
|
+
|
762
|
+
class TestPermissionResponse:
|
763
|
+
"""Test suite for PermissionResponse class."""
|
764
|
+
|
765
|
+
def test_permission_response_granted(self):
|
766
|
+
"""Test PermissionResponse with granted access."""
|
767
|
+
user_roles = ["user", "admin"]
|
768
|
+
user_permissions = {"read", "write", "delete"}
|
769
|
+
required_permissions = ["read", "write"]
|
770
|
+
|
771
|
+
perm_response = PermissionResponse(
|
772
|
+
status=ResponseStatus.SUCCESS,
|
773
|
+
message="Access granted",
|
774
|
+
user_roles=user_roles,
|
775
|
+
user_permissions=user_permissions,
|
776
|
+
effective_permissions=user_permissions,
|
777
|
+
access_granted=True,
|
778
|
+
required_permissions=required_permissions,
|
779
|
+
missing_permissions=[],
|
780
|
+
)
|
781
|
+
|
782
|
+
assert perm_response.status == ResponseStatus.SUCCESS
|
783
|
+
assert perm_response.message == "Access granted"
|
784
|
+
assert perm_response.user_roles == user_roles
|
785
|
+
assert perm_response.user_permissions == user_permissions
|
786
|
+
assert perm_response.effective_permissions == user_permissions
|
787
|
+
assert perm_response.access_granted is True
|
788
|
+
assert perm_response.required_permissions == required_permissions
|
789
|
+
assert perm_response.missing_permissions == []
|
790
|
+
|
791
|
+
def test_permission_response_denied(self):
|
792
|
+
"""Test PermissionResponse with denied access."""
|
793
|
+
user_roles = ["user"]
|
794
|
+
user_permissions = {"read"}
|
795
|
+
required_permissions = ["read", "write", "delete"]
|
796
|
+
missing_permissions = ["write", "delete"]
|
797
|
+
|
798
|
+
perm_response = PermissionResponse(
|
799
|
+
status=ResponseStatus.ERROR,
|
800
|
+
message="Access denied - insufficient permissions",
|
801
|
+
user_roles=user_roles,
|
802
|
+
user_permissions=user_permissions,
|
803
|
+
effective_permissions=user_permissions,
|
804
|
+
access_granted=False,
|
805
|
+
required_permissions=required_permissions,
|
806
|
+
missing_permissions=missing_permissions,
|
807
|
+
)
|
808
|
+
|
809
|
+
assert perm_response.status == ResponseStatus.ERROR
|
810
|
+
assert perm_response.message == "Access denied - insufficient permissions"
|
811
|
+
assert perm_response.user_roles == user_roles
|
812
|
+
assert perm_response.user_permissions == user_permissions
|
813
|
+
assert perm_response.effective_permissions == user_permissions
|
814
|
+
assert perm_response.access_granted is False
|
815
|
+
assert perm_response.required_permissions == required_permissions
|
816
|
+
assert perm_response.missing_permissions == missing_permissions
|
817
|
+
|
818
|
+
def test_permission_response_validation_consistency(self):
|
819
|
+
"""Test PermissionResponse validation consistency."""
|
820
|
+
user_roles = ["user"]
|
821
|
+
user_permissions = {"read"}
|
822
|
+
required_permissions = ["read", "write"]
|
823
|
+
|
824
|
+
# Granted access should have SUCCESS status
|
825
|
+
with pytest.raises(ValidationError) as exc_info:
|
826
|
+
PermissionResponse(
|
827
|
+
status=ResponseStatus.ERROR, # Should be SUCCESS
|
828
|
+
message="Access granted",
|
829
|
+
user_roles=user_roles,
|
830
|
+
user_permissions=user_permissions,
|
831
|
+
effective_permissions=user_permissions,
|
832
|
+
access_granted=True,
|
833
|
+
required_permissions=required_permissions,
|
834
|
+
missing_permissions=[],
|
835
|
+
)
|
836
|
+
|
837
|
+
assert "Granted access must have SUCCESS status" in str(exc_info.value)
|
838
|
+
|
839
|
+
# Denied access should have ERROR status
|
840
|
+
with pytest.raises(ValidationError) as exc_info:
|
841
|
+
PermissionResponse(
|
842
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
843
|
+
message="Access denied",
|
844
|
+
user_roles=user_roles,
|
845
|
+
user_permissions=user_permissions,
|
846
|
+
effective_permissions=user_permissions,
|
847
|
+
access_granted=False,
|
848
|
+
required_permissions=required_permissions,
|
849
|
+
missing_permissions=["write"],
|
850
|
+
)
|
851
|
+
|
852
|
+
assert "Denied access must have ERROR status" in str(exc_info.value)
|
853
|
+
|
854
|
+
def test_permission_response_factory_methods(self):
|
855
|
+
"""Test PermissionResponse factory methods."""
|
856
|
+
user_roles = ["user", "admin"]
|
857
|
+
user_permissions = {"read", "write", "delete"}
|
858
|
+
required_permissions = ["read", "write"]
|
859
|
+
|
860
|
+
# Granted access
|
861
|
+
granted_response = PermissionResponse.create_permission_granted(
|
862
|
+
user_roles, user_permissions, required_permissions
|
863
|
+
)
|
864
|
+
|
865
|
+
assert granted_response.status == ResponseStatus.SUCCESS
|
866
|
+
assert granted_response.message == "Access granted"
|
867
|
+
assert granted_response.user_roles == user_roles
|
868
|
+
assert granted_response.user_permissions == user_permissions
|
869
|
+
assert granted_response.effective_permissions == user_permissions
|
870
|
+
assert granted_response.access_granted is True
|
871
|
+
assert granted_response.required_permissions == required_permissions
|
872
|
+
assert granted_response.missing_permissions == []
|
873
|
+
|
874
|
+
# Denied access
|
875
|
+
denied_response = PermissionResponse.create_permission_denied(
|
876
|
+
user_roles, user_permissions, required_permissions
|
877
|
+
)
|
878
|
+
|
879
|
+
assert denied_response.status == ResponseStatus.ERROR
|
880
|
+
assert denied_response.message == "Access denied - insufficient permissions"
|
881
|
+
assert denied_response.user_roles == user_roles
|
882
|
+
assert denied_response.user_permissions == user_permissions
|
883
|
+
assert denied_response.effective_permissions == user_permissions
|
884
|
+
assert denied_response.access_granted is False
|
885
|
+
assert denied_response.required_permissions == required_permissions
|
886
|
+
assert (
|
887
|
+
denied_response.missing_permissions == []
|
888
|
+
) # User has all required permissions
|
889
|
+
|
890
|
+
|
891
|
+
class TestRateLimitResponse:
|
892
|
+
"""Test suite for RateLimitResponse class."""
|
893
|
+
|
894
|
+
def test_rate_limit_response_within_limit(self):
|
895
|
+
"""Test RateLimitResponse when within rate limit."""
|
896
|
+
now = datetime.now(timezone.utc)
|
897
|
+
rate_limit_status = RateLimitStatus(
|
898
|
+
identifier="192.168.1.1",
|
899
|
+
current_count=5,
|
900
|
+
limit=10,
|
901
|
+
window_start=now,
|
902
|
+
window_end=now + timedelta(minutes=1),
|
903
|
+
is_exceeded=False,
|
904
|
+
remaining_requests=5,
|
905
|
+
reset_time=now + timedelta(minutes=1),
|
906
|
+
window_size_seconds=60,
|
907
|
+
)
|
908
|
+
|
909
|
+
rate_response = RateLimitResponse(
|
910
|
+
status=ResponseStatus.SUCCESS,
|
911
|
+
message="Rate limit status",
|
912
|
+
rate_limit_status=rate_limit_status,
|
913
|
+
usage_percentage=50.0,
|
914
|
+
reset_time=now + timedelta(minutes=1),
|
915
|
+
)
|
916
|
+
|
917
|
+
assert rate_response.status == ResponseStatus.SUCCESS
|
918
|
+
assert rate_response.message == "Rate limit status"
|
919
|
+
assert rate_response.rate_limit_status == rate_limit_status
|
920
|
+
assert rate_response.usage_percentage == 50.0
|
921
|
+
assert rate_response.reset_time == now + timedelta(minutes=1)
|
922
|
+
assert rate_response.retry_after is None
|
923
|
+
|
924
|
+
def test_rate_limit_response_exceeded(self):
|
925
|
+
"""Test RateLimitResponse when rate limit is exceeded."""
|
926
|
+
now = datetime.now(timezone.utc)
|
927
|
+
rate_limit_status = RateLimitStatus(
|
928
|
+
identifier="192.168.1.1",
|
929
|
+
current_count=15,
|
930
|
+
limit=10,
|
931
|
+
window_start=now,
|
932
|
+
window_end=now + timedelta(minutes=1),
|
933
|
+
is_exceeded=True,
|
934
|
+
remaining_requests=0,
|
935
|
+
reset_time=now + timedelta(minutes=1),
|
936
|
+
window_size_seconds=60,
|
937
|
+
)
|
938
|
+
|
939
|
+
rate_response = RateLimitResponse(
|
940
|
+
status=ResponseStatus.ERROR,
|
941
|
+
message="Rate limit exceeded",
|
942
|
+
rate_limit_status=rate_limit_status,
|
943
|
+
usage_percentage=150.0,
|
944
|
+
reset_time=now + timedelta(minutes=1),
|
945
|
+
retry_after=60,
|
946
|
+
)
|
947
|
+
|
948
|
+
assert rate_response.status == ResponseStatus.ERROR
|
949
|
+
assert rate_response.message == "Rate limit exceeded"
|
950
|
+
assert rate_response.rate_limit_status == rate_limit_status
|
951
|
+
assert rate_response.usage_percentage == 150.0
|
952
|
+
assert rate_response.reset_time == now + timedelta(minutes=1)
|
953
|
+
assert rate_response.retry_after == 60
|
954
|
+
|
955
|
+
def test_rate_limit_response_validation_consistency(self):
|
956
|
+
"""Test RateLimitResponse validation consistency."""
|
957
|
+
now = datetime.now(timezone.utc)
|
958
|
+
|
959
|
+
# Exceeded rate limit should have ERROR status
|
960
|
+
rate_limit_status = RateLimitStatus(
|
961
|
+
identifier="192.168.1.1",
|
962
|
+
current_count=15,
|
963
|
+
limit=10,
|
964
|
+
window_start=now,
|
965
|
+
window_end=now + timedelta(minutes=1),
|
966
|
+
is_exceeded=True,
|
967
|
+
remaining_requests=0,
|
968
|
+
reset_time=now + timedelta(minutes=1),
|
969
|
+
window_size_seconds=60,
|
970
|
+
)
|
971
|
+
|
972
|
+
with pytest.raises(ValidationError) as exc_info:
|
973
|
+
RateLimitResponse(
|
974
|
+
status=ResponseStatus.SUCCESS, # Should be ERROR
|
975
|
+
message="Rate limit status",
|
976
|
+
rate_limit_status=rate_limit_status,
|
977
|
+
usage_percentage=150.0,
|
978
|
+
reset_time=now + timedelta(minutes=1),
|
979
|
+
)
|
980
|
+
|
981
|
+
assert "Exceeded rate limit must have ERROR status" in str(exc_info.value)
|
982
|
+
|
983
|
+
# Within rate limit should have SUCCESS status
|
984
|
+
rate_limit_status = RateLimitStatus(
|
985
|
+
identifier="192.168.1.1",
|
986
|
+
current_count=5,
|
987
|
+
limit=10,
|
988
|
+
window_start=now,
|
989
|
+
window_end=now + timedelta(minutes=1),
|
990
|
+
is_exceeded=False,
|
991
|
+
remaining_requests=5,
|
992
|
+
reset_time=now + timedelta(minutes=1),
|
993
|
+
window_size_seconds=60,
|
994
|
+
)
|
995
|
+
|
996
|
+
with pytest.raises(ValidationError) as exc_info:
|
997
|
+
RateLimitResponse(
|
998
|
+
status=ResponseStatus.ERROR, # Should be SUCCESS
|
999
|
+
message="Rate limit exceeded",
|
1000
|
+
rate_limit_status=rate_limit_status,
|
1001
|
+
usage_percentage=50.0,
|
1002
|
+
reset_time=now + timedelta(minutes=1),
|
1003
|
+
)
|
1004
|
+
|
1005
|
+
assert "Within rate limit must have SUCCESS status" in str(exc_info.value)
|
1006
|
+
|
1007
|
+
def test_rate_limit_response_factory_method(self):
|
1008
|
+
"""Test RateLimitResponse factory method."""
|
1009
|
+
now = datetime.now(timezone.utc)
|
1010
|
+
|
1011
|
+
# Within limit
|
1012
|
+
rate_limit_status = RateLimitStatus(
|
1013
|
+
identifier="192.168.1.1",
|
1014
|
+
current_count=5,
|
1015
|
+
limit=10,
|
1016
|
+
window_start=now,
|
1017
|
+
window_end=now + timedelta(minutes=1),
|
1018
|
+
is_exceeded=False,
|
1019
|
+
remaining_requests=5,
|
1020
|
+
reset_time=now + timedelta(minutes=1),
|
1021
|
+
window_size_seconds=60,
|
1022
|
+
)
|
1023
|
+
|
1024
|
+
response = RateLimitResponse.create_rate_limit_status(rate_limit_status)
|
1025
|
+
|
1026
|
+
assert response.status == ResponseStatus.SUCCESS
|
1027
|
+
assert response.message == "Rate limit status"
|
1028
|
+
assert response.rate_limit_status == rate_limit_status
|
1029
|
+
assert response.usage_percentage == 50.0
|
1030
|
+
assert response.reset_time == now + timedelta(minutes=1)
|
1031
|
+
assert response.retry_after is None
|
1032
|
+
|
1033
|
+
# Exceeded limit
|
1034
|
+
rate_limit_status = RateLimitStatus(
|
1035
|
+
identifier="192.168.1.1",
|
1036
|
+
current_count=15,
|
1037
|
+
limit=10,
|
1038
|
+
window_start=now,
|
1039
|
+
window_end=now + timedelta(minutes=1),
|
1040
|
+
is_exceeded=True,
|
1041
|
+
remaining_requests=0,
|
1042
|
+
reset_time=now + timedelta(minutes=1),
|
1043
|
+
window_size_seconds=60,
|
1044
|
+
)
|
1045
|
+
|
1046
|
+
response = RateLimitResponse.create_rate_limit_status(rate_limit_status)
|
1047
|
+
|
1048
|
+
assert response.status == ResponseStatus.ERROR
|
1049
|
+
assert response.message == "Rate limit exceeded"
|
1050
|
+
assert response.rate_limit_status == rate_limit_status
|
1051
|
+
assert response.usage_percentage == 150.0
|
1052
|
+
assert response.reset_time == now + timedelta(minutes=1)
|
1053
|
+
assert response.retry_after is not None
|
1054
|
+
assert response.retry_after >= 59 # Allow for small time differences
|