from __future__ import annotations import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[3])) from backend.app.api.errors import AppError from backend.app.security.path_guard import PathGuard class PathGuardTest(unittest.TestCase): def setUp(self) -> None: self.temp_dir = tempfile.TemporaryDirectory() self.root = Path(self.temp_dir.name) / "root" self.root.mkdir(parents=True, exist_ok=True) self.other = Path(self.temp_dir.name) / "other" self.other.mkdir(parents=True, exist_ok=True) self.guard = PathGuard({"storage1": str(self.root)}) def tearDown(self) -> None: self.temp_dir.cleanup() def test_resolve_under_whitelisted_root(self) -> None: target = self.root / "series" target.mkdir() resolved = self.guard.resolve_directory_path("storage1/series") self.assertEqual(resolved.alias, "storage1") self.assertEqual(resolved.relative, "storage1/series") self.assertEqual(resolved.absolute, target.resolve()) def test_rejects_path_traversal(self) -> None: with self.assertRaises(AppError) as ctx: self.guard.resolve_path("storage1/../etc") self.assertEqual(ctx.exception.code, "path_traversal_detected") self.assertEqual(ctx.exception.status_code, 403) def test_rejects_symlink_escape(self) -> None: outside_dir = self.other / "escape" outside_dir.mkdir() symlink = self.root / "link" symlink.symlink_to(outside_dir, target_is_directory=True) with self.assertRaises(AppError) as ctx: self.guard.resolve_directory_path("storage1/link") self.assertEqual(ctx.exception.code, "path_outside_whitelist") self.assertEqual(ctx.exception.status_code, 403) if __name__ == "__main__": unittest.main()