"""Tests for automaton.dashboard.ui.app.""" from pathlib import Path import io import pytest from automaton.dashboard.ui.app import DashboardHandler def test_validate_task_name() -> None: assert DashboardHandler._validate_task_name("good-task") is True assert DashboardHandler._validate_task_name("bad/../task") is False assert DashboardHandler._validate_task_name("bad\\task") is False assert DashboardHandler._validate_task_name("") is False def test_find_tasks_dir(tmp_path: Path) -> None: (tmp_path / ".automaton" / "tasks").mkdir(parents=True) tasks_dir = DashboardHandler._find_tasks_dir(tmp_path) assert tasks_dir == tmp_path / ".automaton" / "tasks" def test_find_tasks_dir_missing(tmp_path: Path) -> None: tasks_dir = DashboardHandler._find_tasks_dir(tmp_path) assert tasks_dir == tmp_path / ".automaton" / "tasks" def test_path_traversal_attempt() -> None: """Task names with path traversal should be rejected.""" assert DashboardHandler._validate_task_name("../etc/passwd") is False assert DashboardHandler._validate_task_name("task%2f..%2fetc") is False def test_static_path_traversal_symlink(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Static file serving must reject symlinks that resolve outside the html directory.""" html_dir = tmp_path / "html" html_dir.mkdir() outside = tmp_path / "secret.txt" outside.write_text("secret") symlink = html_dir / "link.txt" symlink.symlink_to(outside) monkeypatch.setattr(DashboardHandler, "dashboard_path", html_dir) handler = DashboardHandler.__new__(DashboardHandler) handler.path = "/link.txt" errors: list[tuple[int, str]] = [] def capture_error(code: int, message: str) -> None: errors.append((code, message)) handler._send_error = capture_error handler._serve_static() assert errors == [(403, "Forbidden")] def test_static_valid_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Static file serving returns a valid html file.""" html_dir = tmp_path / "html" html_dir.mkdir() (html_dir / "index.html").write_text("") monkeypatch.setattr(DashboardHandler, "dashboard_path", html_dir) handler = DashboardHandler.__new__(DashboardHandler) handler.path = "/" response_status: list[int] = [] response_headers: list[tuple[str, str]] = [] def fake_send_response(code: int) -> None: response_status.append(code) def fake_send_header(key: str, value: str) -> None: response_headers.append((key, value)) handler.send_response = fake_send_response handler.send_header = fake_send_header handler.end_headers = lambda: None handler.wfile = io.BytesIO() handler._send_error = lambda code, msg: None handler._serve_static() assert response_status == [200] assert any(h[0] == "Content-Type" and h[1] == "text/html" for h in response_headers) assert handler.wfile.getvalue() == b""