mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
* feat: add embedded GCode viewer Adds PrettyGCode as a built-in GCode visualiser embedded directly in the Bambuddy layout, so users can preview and inspect GCode files without leaving the dashboard.
116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Tests for to_absolute_path() path-traversal guard in library routes.
|
|
|
|
Covers three behaviours added/changed by the GCode viewer PR:
|
|
|
|
1. Relative paths that escape base_dir are rejected with ValueError.
|
|
2. Path.is_relative_to() is used instead of startswith(str(base)),
|
|
avoiding the /data/app vs /data/app_evil prefix-confusion bug.
|
|
3. Legacy absolute paths (pre-migration DB rows) are returned verbatim
|
|
instead of raising ValueError.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _call(relative_path, base_dir):
|
|
"""Call to_absolute_path with base_dir patched to *base_dir*."""
|
|
from backend.app.api.routes.library import to_absolute_path
|
|
|
|
with patch("backend.app.api.routes.library.app_settings") as mock_settings:
|
|
mock_settings.base_dir = str(base_dir)
|
|
return to_absolute_path(relative_path)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# None / empty guard
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestNullInputs:
|
|
def test_none_returns_none(self, tmp_path):
|
|
assert _call(None, tmp_path) is None
|
|
|
|
def test_empty_string_returns_none(self, tmp_path):
|
|
assert _call("", tmp_path) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Relative path traversal guard
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRelativePathTraversal:
|
|
def test_normal_relative_path_resolves(self, tmp_path):
|
|
"""A safe relative path resolves to base_dir / rel."""
|
|
base = tmp_path / "data"
|
|
base.mkdir()
|
|
result = _call("files/model.gcode", base)
|
|
assert result == (base / "files" / "model.gcode").resolve()
|
|
|
|
def test_traversal_via_dotdot_raises(self, tmp_path):
|
|
"""../etc/passwd must be rejected."""
|
|
base = tmp_path / "data"
|
|
base.mkdir()
|
|
with pytest.raises(ValueError, match="escapes base directory"):
|
|
_call("../etc/passwd", base)
|
|
|
|
def test_traversal_via_nested_dotdot_raises(self, tmp_path):
|
|
"""files/../../etc/passwd must be rejected."""
|
|
base = tmp_path / "data"
|
|
base.mkdir()
|
|
with pytest.raises(ValueError, match="escapes base directory"):
|
|
_call("files/../../etc/passwd", base)
|
|
|
|
def test_prefix_confusion_is_blocked(self, tmp_path):
|
|
"""Ensure /data/app_evil/secret is not permitted when base is /data/app.
|
|
|
|
A naive startswith(str(base)) check would allow this because
|
|
'/data/app_evil'.startswith('/data/app') is True.
|
|
Path.is_relative_to() must be used instead.
|
|
"""
|
|
# Simulate: base = /tmp/.../data_app, sibling = /tmp/.../data_app_evil
|
|
base = tmp_path / "data_app"
|
|
sibling = tmp_path / "data_app_evil"
|
|
base.mkdir()
|
|
sibling.mkdir()
|
|
|
|
# Construct a relative path that resolves into the *sibling* dir.
|
|
# from base: ../data_app_evil/secret
|
|
with pytest.raises(ValueError, match="escapes base directory"):
|
|
_call("../data_app_evil/secret", base)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Legacy absolute path pass-through
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLegacyAbsolutePaths:
|
|
def test_absolute_path_inside_base_is_returned(self, tmp_path):
|
|
"""An absolute path that happens to be inside base_dir is returned as-is."""
|
|
base = tmp_path / "data"
|
|
base.mkdir()
|
|
abs_path = str(base / "archive" / "old.3mf")
|
|
result = _call(abs_path, base)
|
|
assert result == Path(abs_path).resolve()
|
|
|
|
def test_absolute_path_outside_base_is_returned(self, tmp_path):
|
|
"""An absolute path outside base_dir is returned verbatim (legacy compat).
|
|
|
|
Pre-migration DB rows may store absolute paths that predate the
|
|
base_dir layout. These must NOT raise ValueError; callers are
|
|
responsible for further existence checks.
|
|
"""
|
|
base = tmp_path / "data"
|
|
base.mkdir()
|
|
outside = tmp_path / "old_archive" / "legacy.3mf"
|
|
result = _call(str(outside), base)
|
|
assert result == outside.resolve()
|