89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""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("<html></html>")
|
||
|
|
|
||
|
|
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"<html></html>"
|