Spaces:
Running
Running
| import asyncio | |
| import json | |
| from unittest.mock import Mock | |
| import pytest | |
| from fastapi import Request | |
| from fastapi.exceptions import RequestValidationError | |
| from slowapi.errors import RateLimitExceeded | |
| from app.exceptions import ( | |
| AppException, | |
| ConflictException, | |
| ExternalServiceException, | |
| ForbiddenException, | |
| NotFoundException, | |
| RateLimitException, | |
| UnauthorizedException, | |
| UnsafePromptException, | |
| ValidationException, | |
| ) | |
| from app.main import ( | |
| app_exception_handler, | |
| rate_limit_handler, | |
| unhandled_exception_handler, | |
| validation_exception_handler, | |
| ) | |
| def _mock_request(request_id="test-123"): | |
| request = Mock() | |
| request.state = Mock(request_id=request_id) | |
| return request | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Exception class instantiation (uncovered lines) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_app_exception_defaults(): | |
| exc = AppException("CODE", "msg", 500) | |
| assert exc.code == "CODE" | |
| assert exc.message == "msg" | |
| assert exc.status_code == 500 | |
| assert exc.details == {} | |
| def test_app_exception_with_details(): | |
| exc = AppException("CODE", "msg", 400, {"key": "val"}) | |
| assert exc.details == {"key": "val"} | |
| def test_not_found_with_identifier(): | |
| exc = NotFoundException("document", "42") | |
| assert exc.code == "DOCUMENT_NOT_FOUND" | |
| assert "42" in exc.message | |
| assert exc.status_code == 404 | |
| assert exc.details == {"document": "42"} | |
| def test_not_found_without_identifier(): | |
| exc = NotFoundException("user") | |
| assert exc.code == "USER_NOT_FOUND" | |
| assert exc.status_code == 404 | |
| assert exc.details == {} | |
| def test_unauthorized_default(): | |
| exc = UnauthorizedException() | |
| assert exc.code == "UNAUTHORIZED" | |
| assert exc.status_code == 401 | |
| def test_forbidden_default(): | |
| exc = ForbiddenException() | |
| assert exc.code == "FORBIDDEN" | |
| assert exc.status_code == 403 | |
| def test_conflict(): | |
| exc = ConflictException("Username taken") | |
| assert exc.code == "CONFLICT" | |
| assert exc.status_code == 409 | |
| assert exc.message == "Username taken" | |
| def test_conflict_with_details(): | |
| exc = ConflictException("Conflict", {"field": "username"}) | |
| assert exc.details == {"field": "username"} | |
| def test_validation(): | |
| exc = ValidationException("bad input", {"field": "email"}) | |
| assert exc.code == "VALIDATION_ERROR" | |
| assert exc.status_code == 400 | |
| assert exc.details == {"field": "email"} | |
| def test_rate_limit(): | |
| exc = RateLimitException() | |
| assert exc.code == "RATE_LIMIT_EXCEEDED" | |
| assert exc.status_code == 429 | |
| assert "Rate limit" in exc.message | |
| def test_external_service(): | |
| exc = ExternalServiceException("openai", "key expired") | |
| assert exc.code == "OPENAI_ERROR" | |
| assert exc.status_code == 502 | |
| assert exc.details == {"service": "openai"} | |
| def test_external_service_default_message(): | |
| exc = ExternalServiceException("stripe") | |
| assert "stripe" in exc.message | |
| def test_unsafe_prompt(): | |
| exc = UnsafePromptException() | |
| assert exc.code == "UNSAFE_PROMPT" | |
| assert exc.status_code == 400 | |
| assert "prohibited" in exc.message | |
| def test_unsafe_prompt_custom_message(): | |
| exc = UnsafePromptException("Custom block message") | |
| assert exc.message == "Custom block message" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Exception handler response format (uncovered lines) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_app_exception_handler(): | |
| request = _mock_request() | |
| exc = AppException("NOT_FOUND", "nope", 404, {"id": "1"}) | |
| response = asyncio.run(app_exception_handler(request, exc)) | |
| assert response.status_code == 404 | |
| body = json.loads(response.body) | |
| assert body == { | |
| "error": { | |
| "code": "NOT_FOUND", | |
| "message": "nope", | |
| "details": {"id": "1"}, | |
| "request_id": "test-123", | |
| } | |
| } | |
| def test_app_exception_handler_no_request_id(): | |
| request = Mock() | |
| request.state = Mock(spec_set=[]) | |
| response = asyncio.run( | |
| app_exception_handler(request, AppException("E", "e", 500)) | |
| ) | |
| body = json.loads(response.body) | |
| assert body["error"]["request_id"] is None | |
| def test_rate_limit_handler(): | |
| request = _mock_request() | |
| exc = RateLimitExceeded() | |
| response = asyncio.run(rate_limit_handler(request, exc)) | |
| assert response.status_code == 429 | |
| body = json.loads(response.body) | |
| assert body["error"]["code"] == "RATE_LIMIT_EXCEEDED" | |
| assert body["error"]["request_id"] == "test-123" | |
| def test_validation_exception_handler(): | |
| request = _mock_request() | |
| exc = RequestValidationError( | |
| errors=[ | |
| { | |
| "loc": ("body", "email"), | |
| "msg": "field required", | |
| "type": "value_error", | |
| } | |
| ] | |
| ) | |
| response = asyncio.run(validation_exception_handler(request, exc)) | |
| assert response.status_code == 422 | |
| body = json.loads(response.body) | |
| assert body["error"]["code"] == "VALIDATION_ERROR" | |
| assert body["error"]["details"]["errors"][0]["field"] == "body -> email" | |
| def test_unhandled_exception_handler(monkeypatch): | |
| monkeypatch.setattr("app.main.settings.DEBUG", False) | |
| request = _mock_request() | |
| exc = ValueError("unexpected") | |
| response = asyncio.run(unhandled_exception_handler(request, exc)) | |
| assert response.status_code == 500 | |
| body = json.loads(response.body) | |
| assert body["error"]["code"] == "INTERNAL_ERROR" | |
| assert body["error"]["message"] == "An unexpected error occurred" | |
| def test_unhandled_exception_handler_debug_raises(monkeypatch): | |
| monkeypatch.setattr("app.main.settings.DEBUG", True) | |
| request = _mock_request() | |
| exc = ValueError("unexpected") | |
| try: | |
| raise exc | |
| except ValueError: | |
| with pytest.raises(ValueError, match="unexpected"): | |
| asyncio.run(unhandled_exception_handler(request, exc)) | |