"""Tests for mixins/http.py edge cases. Unit tests for HTTP routing and response handling. """ import io import json import tempfile import unittest from pathlib import Path from unittest.mock import patch, MagicMock from amc_server.mixins.http import HttpMixin class DummyHttpHandler(HttpMixin): """Minimal handler for testing HTTP mixin.""" def __init__(self): self.response_code = None self.headers_sent = {} self.body_sent = b"" self.path = "/" self.wfile = io.BytesIO() def send_response(self, code): self.response_code = code def send_header(self, key, value): self.headers_sent[key] = value def end_headers(self): pass class TestSendBytesResponse(unittest.TestCase): """Tests for _send_bytes_response edge cases.""" def test_sends_correct_headers(self): handler = DummyHttpHandler() handler._send_bytes_response(200, b"test", content_type="text/plain") self.assertEqual(handler.response_code, 200) self.assertEqual(handler.headers_sent["Content-Type"], "text/plain") self.assertEqual(handler.headers_sent["Content-Length"], "4") def test_includes_extra_headers(self): handler = DummyHttpHandler() handler._send_bytes_response( 200, b"test", extra_headers={"X-Custom": "value", "Cache-Control": "no-cache"} ) self.assertEqual(handler.headers_sent["X-Custom"], "value") self.assertEqual(handler.headers_sent["Cache-Control"], "no-cache") def test_broken_pipe_returns_false(self): handler = DummyHttpHandler() handler.wfile.write = MagicMock(side_effect=BrokenPipeError()) result = handler._send_bytes_response(200, b"test") self.assertFalse(result) def test_connection_reset_returns_false(self): handler = DummyHttpHandler() handler.wfile.write = MagicMock(side_effect=ConnectionResetError()) result = handler._send_bytes_response(200, b"test") self.assertFalse(result) def test_os_error_returns_false(self): handler = DummyHttpHandler() handler.wfile.write = MagicMock(side_effect=OSError("write error")) result = handler._send_bytes_response(200, b"test") self.assertFalse(result) class TestSendJson(unittest.TestCase): """Tests for _send_json edge cases.""" def test_includes_cors_header(self): handler = DummyHttpHandler() handler._send_json(200, {"key": "value"}) self.assertEqual(handler.headers_sent["Access-Control-Allow-Origin"], "*") def test_sets_json_content_type(self): handler = DummyHttpHandler() handler._send_json(200, {"key": "value"}) self.assertEqual(handler.headers_sent["Content-Type"], "application/json") def test_encodes_payload_as_json(self): handler = DummyHttpHandler() handler._send_json(200, {"key": "value"}) written = handler.wfile.getvalue() self.assertEqual(json.loads(written), {"key": "value"}) class TestServeDashboardFile(unittest.TestCase): """Tests for _serve_dashboard_file edge cases.""" def test_nonexistent_file_returns_404(self): handler = DummyHttpHandler() handler.errors = [] def capture_error(code, message): handler.errors.append((code, message)) handler._json_error = capture_error with tempfile.TemporaryDirectory() as tmpdir: with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("nonexistent.html") self.assertEqual(len(handler.errors), 1) self.assertEqual(handler.errors[0][0], 404) def test_path_traversal_blocked(self): handler = DummyHttpHandler() handler.errors = [] def capture_error(code, message): handler.errors.append((code, message)) handler._json_error = capture_error with tempfile.TemporaryDirectory() as tmpdir: # Create a file outside the dashboard dir that shouldn't be accessible secret = Path(tmpdir).parent / "secret.txt" with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("../secret.txt") self.assertEqual(len(handler.errors), 1) self.assertEqual(handler.errors[0][0], 403) def test_correct_content_type_for_html(self): handler = DummyHttpHandler() with tempfile.TemporaryDirectory() as tmpdir: html_file = Path(tmpdir) / "test.html" html_file.write_text("") with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("test.html") self.assertEqual(handler.headers_sent["Content-Type"], "text/html; charset=utf-8") def test_correct_content_type_for_css(self): handler = DummyHttpHandler() with tempfile.TemporaryDirectory() as tmpdir: css_file = Path(tmpdir) / "styles.css" css_file.write_text("body {}") with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("styles.css") self.assertEqual(handler.headers_sent["Content-Type"], "text/css; charset=utf-8") def test_correct_content_type_for_js(self): handler = DummyHttpHandler() with tempfile.TemporaryDirectory() as tmpdir: js_file = Path(tmpdir) / "app.js" js_file.write_text("console.log('hello')") with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("app.js") self.assertEqual(handler.headers_sent["Content-Type"], "application/javascript; charset=utf-8") def test_unknown_extension_gets_octet_stream(self): handler = DummyHttpHandler() with tempfile.TemporaryDirectory() as tmpdir: unknown_file = Path(tmpdir) / "data.xyz" unknown_file.write_bytes(b"\x00\x01\x02") with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("data.xyz") self.assertEqual(handler.headers_sent["Content-Type"], "application/octet-stream") def test_no_cache_headers_set(self): handler = DummyHttpHandler() with tempfile.TemporaryDirectory() as tmpdir: html_file = Path(tmpdir) / "test.html" html_file.write_text("") with patch("amc_server.mixins.http.DASHBOARD_DIR", Path(tmpdir)): handler._serve_dashboard_file("test.html") self.assertIn("no-cache", handler.headers_sent.get("Cache-Control", "")) class TestDoGet(unittest.TestCase): """Tests for do_GET routing edge cases.""" def _make_handler(self, path): handler = DummyHttpHandler() handler.path = path handler._serve_dashboard_file = MagicMock() handler._serve_state = MagicMock() handler._serve_stream = MagicMock() handler._serve_events = MagicMock() handler._serve_conversation = MagicMock() handler._json_error = MagicMock() return handler def test_root_serves_index(self): handler = self._make_handler("/") handler.do_GET() handler._serve_dashboard_file.assert_called_with("index.html") def test_index_html_serves_index(self): handler = self._make_handler("/index.html") handler.do_GET() handler._serve_dashboard_file.assert_called_with("index.html") def test_static_file_served(self): handler = self._make_handler("/components/App.js") handler.do_GET() handler._serve_dashboard_file.assert_called_with("components/App.js") def test_path_traversal_in_static_blocked(self): handler = self._make_handler("/../../etc/passwd") handler.do_GET() handler._json_error.assert_called_with(404, "Not Found") def test_api_state_routed(self): handler = self._make_handler("/api/state") handler.do_GET() handler._serve_state.assert_called_once() def test_api_stream_routed(self): handler = self._make_handler("/api/stream") handler.do_GET() handler._serve_stream.assert_called_once() def test_api_events_routed_with_id(self): handler = self._make_handler("/api/events/session-123") handler.do_GET() handler._serve_events.assert_called_with("session-123") def test_api_events_url_decoded(self): handler = self._make_handler("/api/events/session%20with%20spaces") handler.do_GET() handler._serve_events.assert_called_with("session with spaces") def test_api_conversation_with_query_params(self): handler = self._make_handler("/api/conversation/sess123?project_dir=/test&agent=codex") handler.do_GET() handler._serve_conversation.assert_called_with("sess123", "/test", "codex") def test_api_conversation_defaults_to_claude(self): handler = self._make_handler("/api/conversation/sess123") handler.do_GET() handler._serve_conversation.assert_called_with("sess123", "", "claude") def test_unknown_api_path_returns_404(self): handler = self._make_handler("/api/unknown") handler.do_GET() handler._json_error.assert_called_with(404, "Not Found") class TestDoGetSpawnRoutes(unittest.TestCase): """Tests for spawn-related GET routes.""" def _make_handler(self, path): handler = DummyHttpHandler() handler.path = path handler._serve_dashboard_file = MagicMock() handler._serve_state = MagicMock() handler._serve_stream = MagicMock() handler._serve_events = MagicMock() handler._serve_conversation = MagicMock() handler._serve_skills = MagicMock() handler._handle_projects = MagicMock() handler._handle_health = MagicMock() handler._json_error = MagicMock() return handler def test_api_projects_routed(self): handler = self._make_handler("/api/projects") handler.do_GET() handler._handle_projects.assert_called_once() def test_api_health_routed(self): handler = self._make_handler("/api/health") handler.do_GET() handler._handle_health.assert_called_once() class TestDoPost(unittest.TestCase): """Tests for do_POST routing edge cases.""" def _make_handler(self, path): handler = DummyHttpHandler() handler.path = path handler._dismiss_dead_sessions = MagicMock() handler._dismiss_session = MagicMock() handler._respond_to_session = MagicMock() handler._handle_spawn = MagicMock() handler._handle_projects_refresh = MagicMock() handler._json_error = MagicMock() return handler def test_dismiss_dead_routed(self): handler = self._make_handler("/api/dismiss-dead") handler.do_POST() handler._dismiss_dead_sessions.assert_called_once() def test_dismiss_session_routed(self): handler = self._make_handler("/api/dismiss/session-abc") handler.do_POST() handler._dismiss_session.assert_called_with("session-abc") def test_dismiss_url_decoded(self): handler = self._make_handler("/api/dismiss/session%2Fwith%2Fslash") handler.do_POST() handler._dismiss_session.assert_called_with("session/with/slash") def test_respond_routed(self): handler = self._make_handler("/api/respond/session-xyz") handler.do_POST() handler._respond_to_session.assert_called_with("session-xyz") def test_spawn_routed(self): handler = self._make_handler("/api/spawn") handler.do_POST() handler._handle_spawn.assert_called_once() def test_projects_refresh_routed(self): handler = self._make_handler("/api/projects/refresh") handler.do_POST() handler._handle_projects_refresh.assert_called_once() def test_unknown_post_path_returns_404(self): handler = self._make_handler("/api/unknown") handler.do_POST() handler._json_error.assert_called_with(404, "Not Found") class TestDoOptions(unittest.TestCase): """Tests for do_OPTIONS CORS preflight.""" def test_returns_204_with_cors_headers(self): handler = DummyHttpHandler() handler.do_OPTIONS() self.assertEqual(handler.response_code, 204) self.assertEqual(handler.headers_sent["Access-Control-Allow-Origin"], "*") self.assertIn("POST", handler.headers_sent["Access-Control-Allow-Methods"]) self.assertIn("GET", handler.headers_sent["Access-Control-Allow-Methods"]) self.assertIn("Content-Type", handler.headers_sent["Access-Control-Allow-Headers"]) self.assertIn("Authorization", handler.headers_sent["Access-Control-Allow-Headers"]) class TestJsonError(unittest.TestCase): """Tests for _json_error helper.""" def test_sends_json_with_error(self): handler = DummyHttpHandler() handler._json_error(404, "Not Found") written = handler.wfile.getvalue() payload = json.loads(written) self.assertEqual(payload, {"ok": False, "error": "Not Found"}) if __name__ == "__main__": unittest.main()