test(dashboard): cover constants, git helper, cron endpoint, files sandbox
This commit is contained in:
107
tests/test_dashboard_files_sandbox.py
Normal file
107
tests/test_dashboard_files_sandbox.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for the /api/files sandbox — _resolve_sandboxed must block path traversal."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
DASH = PROJECT_ROOT / "dashboard"
|
||||
|
||||
if str(DASH) not in sys.path:
|
||||
sys.path.insert(0, str(DASH))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def files_module():
|
||||
from handlers import files as _f # type: ignore
|
||||
return _f
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handler(files_module):
|
||||
class _Stub(files_module.FilesHandlers):
|
||||
def __init__(self):
|
||||
self.captured = None
|
||||
self.captured_code = None
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.captured = data
|
||||
self.captured_code = code
|
||||
|
||||
return _Stub()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sandboxed(tmp_path, monkeypatch):
|
||||
"""Replace ALLOWED_WORKSPACES with a single tmp_path to isolate tests."""
|
||||
import constants # type: ignore
|
||||
root = tmp_path / "repo"
|
||||
root.mkdir()
|
||||
monkeypatch.setattr(constants, "ALLOWED_WORKSPACES", [root])
|
||||
return root
|
||||
|
||||
|
||||
def test_resolve_accepts_file_inside_workspace(handler, sandboxed):
|
||||
(sandboxed / "notes.md").write_text("hello", encoding="utf-8")
|
||||
target, workspace = handler._resolve_sandboxed("notes.md")
|
||||
assert target is not None
|
||||
assert target == (sandboxed / "notes.md").resolve()
|
||||
assert workspace == sandboxed
|
||||
|
||||
|
||||
def test_resolve_accepts_nested_file(handler, sandboxed):
|
||||
(sandboxed / "sub").mkdir()
|
||||
(sandboxed / "sub" / "file.txt").write_text("x", encoding="utf-8")
|
||||
target, workspace = handler._resolve_sandboxed("sub/file.txt")
|
||||
assert target == (sandboxed / "sub" / "file.txt").resolve()
|
||||
assert workspace == sandboxed
|
||||
|
||||
|
||||
def test_resolve_rejects_parent_traversal(handler, sandboxed):
|
||||
"""../etc/passwd-style requests must resolve OUTSIDE the workspace and
|
||||
be refused by returning (None, None)."""
|
||||
target, workspace = handler._resolve_sandboxed("../../../etc/passwd")
|
||||
assert target is None
|
||||
assert workspace is None
|
||||
|
||||
|
||||
def test_resolve_rejects_absolute_escape(handler, sandboxed):
|
||||
"""Absolute path that lands outside the workspace must be refused.
|
||||
|
||||
Note: Path('/base') / '/etc/passwd' == Path('/etc/passwd') because the
|
||||
second path is absolute. The resolver must still refuse it.
|
||||
"""
|
||||
target, workspace = handler._resolve_sandboxed("/etc/passwd")
|
||||
assert target is None, "absolute outside path must be refused"
|
||||
assert workspace is None
|
||||
|
||||
|
||||
def test_handle_files_get_returns_403_on_denied(handler, sandboxed):
|
||||
"""End-to-end: /api/files with a traversal path must 403."""
|
||||
# Simulate HTTP path parsing: the endpoint reads self.path
|
||||
handler.path = "/api/files?path=../../../etc/passwd&action=list"
|
||||
handler.handle_files_get()
|
||||
assert handler.captured_code == 403
|
||||
assert handler.captured["error"] == "Access denied"
|
||||
|
||||
|
||||
def test_handle_files_get_reads_a_file(handler, sandboxed):
|
||||
(sandboxed / "hello.txt").write_text("hi there", encoding="utf-8")
|
||||
handler.path = "/api/files?path=hello.txt&action=list"
|
||||
handler.handle_files_get()
|
||||
assert handler.captured_code == 200 or handler.captured_code is None
|
||||
assert handler.captured["type"] == "file"
|
||||
assert handler.captured["content"] == "hi there"
|
||||
|
||||
|
||||
def test_handle_files_get_lists_a_dir(handler, sandboxed):
|
||||
(sandboxed / "a.md").write_text("a", encoding="utf-8")
|
||||
(sandboxed / "b.md").write_text("b", encoding="utf-8")
|
||||
(sandboxed / "nested").mkdir()
|
||||
handler.path = "/api/files?path=&action=list"
|
||||
handler.handle_files_get()
|
||||
assert handler.captured["type"] == "dir"
|
||||
names = {item["name"] for item in handler.captured["items"]}
|
||||
assert names == {"a.md", "b.md", "nested"}
|
||||
Reference in New Issue
Block a user