Harden framework: tests, VRAM Python, dashboard spec, security, CI
- Rewrite vram_detect in Python with fixed config parsing and 10KB read limit
- Add pytest suite (72 tests) covering dashboard core, app security, and VRAM
- Standardize all prompts to .automaton/tasks/{task-name}/ path
- Reconcile dashboard spec with web implementation; remove themes.py
- Remove half-implemented refresh.py file watcher
- Harden dashboard static-file serving and task-name validation
- Add uncommitted-change guard to update.sh and real Gitea URLs
- Add AGENTS.md, Gitea CI workflow, and template documentation
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""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>"
|
||||
Reference in New Issue
Block a user