| |
|
|
| """ |
| Unit tests for exception handlers. |
| Tests validation error handling and debug logging integration. |
| """ |
|
|
| import pytest |
| from unittest.mock import AsyncMock, MagicMock, patch |
| from fastapi import Request |
| from fastapi.exceptions import RequestValidationError |
|
|
|
|
| class TestSanitizeValidationErrors: |
| """Tests for sanitize_validation_errors function.""" |
| |
| def test_sanitizes_bytes_in_input_field(self): |
| """ |
| What it does: Verifies that bytes in 'input' field are converted to strings. |
| Purpose: Ensure JSON serialization works for bytes objects. |
| """ |
| print("Setup: Creating error with bytes in input field...") |
| from kiro.exceptions import sanitize_validation_errors |
| |
| errors = [ |
| { |
| "type": "json_invalid", |
| "loc": ["body", 0], |
| "msg": "Invalid JSON", |
| "input": b'{"invalid": json}' |
| } |
| ] |
| |
| print("Action: Calling sanitize_validation_errors...") |
| result = sanitize_validation_errors(errors) |
| |
| print(f"Comparing input type: Expected str, Got {type(result[0]['input'])}") |
| assert isinstance(result[0]["input"], str) |
| assert result[0]["input"] == '{"invalid": json}' |
| |
| def test_sanitizes_bytes_in_list_values(self): |
| """ |
| What it does: Verifies that bytes in list values are converted to strings. |
| Purpose: Ensure nested bytes are handled. |
| """ |
| print("Setup: Creating error with bytes in list...") |
| from kiro.exceptions import sanitize_validation_errors |
| |
| errors = [ |
| { |
| "type": "value_error", |
| "loc": ["body", "messages"], |
| "msg": "Invalid value", |
| "input": [b'bytes1', "string", b'bytes2'] |
| } |
| ] |
| |
| print("Action: Calling sanitize_validation_errors...") |
| result = sanitize_validation_errors(errors) |
| |
| print(f"Checking list values are converted...") |
| assert result[0]["input"] == ["bytes1", "string", "bytes2"] |
| |
| def test_preserves_non_bytes_values(self): |
| """ |
| What it does: Verifies that non-bytes values are preserved. |
| Purpose: Ensure normal values are not modified. |
| """ |
| print("Setup: Creating error with normal values...") |
| from kiro.exceptions import sanitize_validation_errors |
| |
| errors = [ |
| { |
| "type": "missing", |
| "loc": ["body", "model"], |
| "msg": "Field required", |
| "input": {"messages": []} |
| } |
| ] |
| |
| print("Action: Calling sanitize_validation_errors...") |
| result = sanitize_validation_errors(errors) |
| |
| print(f"Checking values are preserved...") |
| assert result[0]["input"] == {"messages": []} |
| assert result[0]["type"] == "missing" |
|
|
|
|
| class TestValidationExceptionHandler: |
| """Tests for validation_exception_handler function.""" |
| |
| @pytest.mark.asyncio |
| async def test_returns_422_status_code(self): |
| """ |
| What it does: Verifies that handler returns 422 status code. |
| Purpose: Ensure proper HTTP status for validation errors. |
| """ |
| print("Setup: Creating mock request and exception...") |
| from kiro.exceptions import validation_exception_handler |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=b'{"invalid": json}') |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "json_invalid", "loc": ["body"], "msg": "Invalid JSON", "input": {}} |
| ] |
| |
| |
| with patch('kiro.debug_logger.debug_logger') as mock_logger: |
| print("Action: Calling validation_exception_handler...") |
| response = await validation_exception_handler(mock_request, mock_exc) |
| |
| print(f"Comparing status_code: Expected 422, Got {response.status_code}") |
| assert response.status_code == 422 |
| |
| @pytest.mark.asyncio |
| async def test_calls_flush_on_error_with_422(self): |
| """ |
| What it does: Verifies that handler calls flush_on_error(422). |
| Purpose: Ensure debug logs are flushed for validation errors. |
| """ |
| print("Setup: Creating mock request and exception...") |
| from kiro.exceptions import validation_exception_handler |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=b'{"test": "data"}') |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "missing", "loc": ["body", "model"], "msg": "Field required", "input": {}} |
| ] |
| |
| |
| with patch('kiro.debug_logger.debug_logger') as mock_logger: |
| print("Action: Calling validation_exception_handler...") |
| await validation_exception_handler(mock_request, mock_exc) |
| |
| print("Verifying flush_on_error was called with 422...") |
| mock_logger.flush_on_error.assert_called_once() |
| call_args = mock_logger.flush_on_error.call_args |
| assert call_args[0][0] == 422 |
| |
| @pytest.mark.asyncio |
| async def test_includes_sanitized_errors_in_response(self): |
| """ |
| What it does: Verifies that response includes sanitized errors. |
| Purpose: Ensure error details are returned to client. |
| """ |
| print("Setup: Creating mock request and exception...") |
| from kiro.exceptions import validation_exception_handler |
| import json |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=b'{"test": "data"}') |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "missing", "loc": ["body", "model"], "msg": "Field required", "input": {}} |
| ] |
| |
| with patch('kiro.debug_logger.debug_logger'): |
| print("Action: Calling validation_exception_handler...") |
| response = await validation_exception_handler(mock_request, mock_exc) |
| |
| print("Parsing response body...") |
| body = json.loads(response.body.decode()) |
| |
| print(f"Verifying 'detail' is in response...") |
| assert "detail" in body |
| assert len(body["detail"]) == 1 |
| assert body["detail"][0]["type"] == "missing" |
| |
| @pytest.mark.asyncio |
| async def test_truncates_body_in_response(self): |
| """ |
| What it does: Verifies that body is truncated to 500 chars in response. |
| Purpose: Ensure large bodies don't bloat error responses. |
| """ |
| print("Setup: Creating mock request with large body...") |
| from kiro.exceptions import validation_exception_handler |
| import json |
| |
| large_body = b'{"data": "' + b'x' * 1000 + b'"}' |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=large_body) |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "json_invalid", "loc": ["body"], "msg": "Invalid", "input": {}} |
| ] |
| |
| with patch('kiro.debug_logger.debug_logger'): |
| print("Action: Calling validation_exception_handler...") |
| response = await validation_exception_handler(mock_request, mock_exc) |
| |
| print("Parsing response body...") |
| body = json.loads(response.body.decode()) |
| |
| print(f"Verifying body is truncated to 500 chars...") |
| assert len(body["body"]) <= 500 |
|
|
|
|
| class TestValidationExceptionHandlerLogging: |
| """Tests for logging behavior in validation_exception_handler.""" |
| |
| @pytest.mark.asyncio |
| async def test_logs_error_at_error_level(self): |
| """ |
| What it does: Verifies that validation error is logged at ERROR level. |
| Purpose: Ensure errors are visible in logs. |
| """ |
| print("Setup: Creating mock request and exception...") |
| from kiro.exceptions import validation_exception_handler |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=b'{"test": "data"}') |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "missing", "loc": ["body", "model"], "msg": "Field required", "input": {}} |
| ] |
| |
| with patch('kiro.debug_logger.debug_logger'): |
| with patch('kiro.exceptions.logger') as mock_logger: |
| print("Action: Calling validation_exception_handler...") |
| await validation_exception_handler(mock_request, mock_exc) |
| |
| print("Verifying logger.error was called...") |
| mock_logger.error.assert_called() |
|
|
|
|
| class TestValidationExceptionHandlerEdgeCases: |
| """Tests for edge cases in validation_exception_handler.""" |
| |
| @pytest.mark.asyncio |
| async def test_handles_empty_errors_list(self): |
| """ |
| What it does: Verifies that handler works with empty errors list. |
| Purpose: Ensure edge case doesn't cause crash. |
| """ |
| print("Setup: Creating mock request with empty errors...") |
| from kiro.exceptions import validation_exception_handler |
| import json |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=b'{}') |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [] |
| |
| with patch('kiro.debug_logger.debug_logger'): |
| print("Action: Calling validation_exception_handler...") |
| response = await validation_exception_handler(mock_request, mock_exc) |
| |
| print(f"Verifying response is valid...") |
| assert response.status_code == 422 |
| |
| body = json.loads(response.body.decode()) |
| assert body["detail"] == [] |
| |
| @pytest.mark.asyncio |
| async def test_handles_unicode_in_body(self): |
| """ |
| What it does: Verifies that handler works with unicode in body. |
| Purpose: Ensure international characters are handled. |
| """ |
| print("Setup: Creating mock request with unicode body...") |
| from kiro.exceptions import validation_exception_handler |
| import json |
| |
| unicode_body = '{"message": "Привет мир 🌍"}'.encode('utf-8') |
| |
| mock_request = MagicMock(spec=Request) |
| mock_request.body = AsyncMock(return_value=unicode_body) |
| |
| mock_exc = MagicMock(spec=RequestValidationError) |
| mock_exc.errors.return_value = [ |
| {"type": "missing", "loc": ["body", "model"], "msg": "Field required", "input": {}} |
| ] |
| |
| with patch('kiro.debug_logger.debug_logger'): |
| print("Action: Calling validation_exception_handler...") |
| response = await validation_exception_handler(mock_request, mock_exc) |
| |
| print(f"Verifying response is valid...") |
| assert response.status_code == 422 |
| |
| body = json.loads(response.body.decode()) |
| assert "Привет мир" in body["body"] |
|
|