"""Unit tests for error controller module. Tests custom 404 and 500 error handlers for both API and HTML responses. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import HTTPException, Request from src.server.controllers.error_controller import ( not_found_handler, server_error_handler, ) def _make_request(path: str = "/") -> MagicMock: """Create a mock Request object with a given path.""" request = MagicMock(spec=Request) url = MagicMock() url.path = path request.url = url return request class TestNotFoundHandler: """Tests for the 404 not_found_handler.""" @pytest.mark.asyncio async def test_api_path_returns_json(self): """API paths get a JSON response with 404 status.""" request = _make_request("/api/anime/123") exc = HTTPException(status_code=404) resp = await not_found_handler(request, exc) assert resp.status_code == 404 assert resp.body is not None @pytest.mark.asyncio @patch("src.server.controllers.error_controller.render_template") async def test_web_path_renders_template(self, mock_render): """Non-API paths render the error template.""" mock_render.return_value = MagicMock(status_code=404) request = _make_request("/anime/details") exc = HTTPException(status_code=404) await not_found_handler(request, exc) mock_render.assert_called_once_with( "error.html", request, context={"error": "Page not found", "status_code": 404}, title="404 - Not Found", ) @pytest.mark.asyncio async def test_api_json_structure(self): """API 404 response has 'detail' field.""" import json request = _make_request("/api/missing") exc = HTTPException(status_code=404) resp = await not_found_handler(request, exc) body = json.loads(resp.body) assert body["detail"] == "API endpoint not found" class TestServerErrorHandler: """Tests for the 500 server_error_handler.""" @pytest.mark.asyncio async def test_api_path_returns_json(self): """API paths get a JSON response with 500 status.""" request = _make_request("/api/download") exc = RuntimeError("crash") resp = await server_error_handler(request, exc) assert resp.status_code == 500 @pytest.mark.asyncio @patch("src.server.controllers.error_controller.render_template") async def test_web_path_renders_template(self, mock_render): """Non-API paths render the error template.""" mock_render.return_value = MagicMock(status_code=500) request = _make_request("/settings") exc = RuntimeError("crash") await server_error_handler(request, exc) mock_render.assert_called_once_with( "error.html", request, context={"error": "Internal server error", "status_code": 500}, title="500 - Server Error", ) @pytest.mark.asyncio async def test_api_error_does_not_expose_stack_trace(self): """API 500 response doesn't contain the actual error message.""" import json request = _make_request("/api/vulnerable") exc = RuntimeError("secret database credentials") resp = await server_error_handler(request, exc) body = json.loads(resp.body) assert "secret" not in body["detail"] assert body["detail"] == "Internal server error" @pytest.mark.asyncio async def test_api_path_detection(self): """Correctly distinguishes API vs web paths.""" api_request = _make_request("/api/test") web_request = _make_request("/dashboard") # API path returns JSONResponse from fastapi.responses import JSONResponse resp = await server_error_handler(api_request, Exception("err")) assert isinstance(resp, JSONResponse)