"""路径安全校验工具单元测试 — 路径遍历防护.""" from __future__ import annotations import os import sys import tempfile import unittest from pathlib import Path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "apps", "worker")) from video_processing.path_security import ( # noqa: E402 PathSecurityError, get_allowed_local_dirs, is_in_allowed_dirs, is_path_safe, safe_resolve_path, sanitize_filename, validate_local_schema_path, ) class TestSafeResolvePath(unittest.TestCase): """安全路径解析测试.""" def setUp(self): self.tmpdir = tempfile.mkdtemp() def tearDown(self): import shutil shutil.rmtree(self.tmpdir, ignore_errors=True) # ── 正常路径 ───────────────────────────────────────────────────────── def test_simple_relative_path(self): """简单相对路径应该正常解析.""" result = safe_resolve_path("test.mp4", self.tmpdir) self.assertEqual(result.name, "test.mp4") self.assertTrue(str(result).startswith(self.tmpdir)) def test_subdirectory_path(self): """子目录路径应该正常解析.""" result = safe_resolve_path("sub/dir/file.mp4", self.tmpdir) self.assertTrue(str(result).startswith(self.tmpdir)) self.assertIn("sub/dir/file.mp4", str(result).replace("\\", "/")) def test_dot_slash_path(self): """./ 开头的路径应该正常解析.""" result = safe_resolve_path("./test.mp4", self.tmpdir) self.assertEqual(result.name, "test.mp4") # ── 路径遍历防护 ───────────────────────────────────────────────────── def test_parent_traversal_rejected(self): """../ 路径遍历应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("../etc/passwd", self.tmpdir) def test_multiple_parent_traversal_rejected(self): """多级 ../ 遍历应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("../../etc/passwd", self.tmpdir) def test_mixed_traversal_rejected(self): """混合路径遍历应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("./sub/../../etc/shadow", self.tmpdir) def test_absolute_path_rejected(self): """绝对路径(超出基目录)应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("/etc/passwd", self.tmpdir) # ── 空字节注入 ─────────────────────────────────────────────────────── def test_null_byte_rejected(self): """空字节注入应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("test\x00.mp4", self.tmpdir) # ── 空路径 ────────────────────────────────────────────────────────── def test_empty_path_rejected(self): """空路径应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path("", self.tmpdir) def test_none_path_rejected(self): """None 路径应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path(None, self.tmpdir) # type: ignore def test_whitespace_path_rejected(self): """空白路径应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path(" ", self.tmpdir) # ── 路径长度 ──────────────────────────────────────────────────────── def test_too_long_path_rejected(self): """超长路径应该被拒绝.""" long_path = "a" * 5000 + ".mp4" with self.assertRaises(PathSecurityError): safe_resolve_path(long_path, self.tmpdir) # ── 系统路径防护 ───────────────────────────────────────────────────── def test_proc_path_rejected_when_absolute(self): """/proc/ 路径在绝对路径模式下应该被拒绝(因为超出基目录).""" with self.assertRaises(PathSecurityError): safe_resolve_path("/proc/self/environ", self.tmpdir) # ── 扩展名校验 ─────────────────────────────────────────────────────── def test_extension_whitelist_pass(self): """白名单内的扩展名应该通过.""" result = safe_resolve_path( "test.mp4", self.tmpdir, allowed_extensions={".mp4", ".mov"}, ) self.assertEqual(result.suffix.lower(), ".mp4") def test_extension_whitelist_reject(self): """白名单外的扩展名应该被拒绝.""" with self.assertRaises(PathSecurityError): safe_resolve_path( "test.exe", self.tmpdir, allowed_extensions={".mp4", ".mov"}, ) class TestLocalSchemaPath(unittest.TestCase): """local:// schema 路径测试.""" def setUp(self): self.tmpdir = tempfile.mkdtemp() def tearDown(self): import shutil shutil.rmtree(self.tmpdir, ignore_errors=True) def test_valid_local_schema(self): """有效的 local:// 相对路径应该通过.""" # 创建测试文件 test_file = Path(self.tmpdir) / "test.mp4" test_file.touch() result = validate_local_schema_path("local://test.mp4", self.tmpdir) self.assertTrue(result.exists()) def test_local_schema_absolute_rejected(self): """local:// + 绝对路径应该被拒绝.""" with self.assertRaises(PathSecurityError): validate_local_schema_path("local:///etc/passwd", self.tmpdir) def test_local_schema_traversal_rejected(self): """local:// + 路径遍历应该被拒绝.""" with self.assertRaises(PathSecurityError): validate_local_schema_path("local://../etc/passwd", self.tmpdir) def test_non_local_schema_rejected(self): """非 local:// 开头的路径应该被拒绝.""" with self.assertRaises(PathSecurityError): validate_local_schema_path("http://example.com/test", self.tmpdir) class TestSanitizeFilename(unittest.TestCase): """文件名清理测试.""" def test_normal_filename(self): """正常文件名应该保持不变.""" self.assertEqual(sanitize_filename("video.mp4"), "video.mp4") def test_path_separators_removed(self): """路径分隔符应该被替换.""" self.assertNotIn("/", sanitize_filename("../path/to/file.mp4")) self.assertNotIn("\\", sanitize_filename("..\\path\\file.mp4")) def test_leading_dots_removed(self): """开头的点应该被移除.""" result = sanitize_filename(".hidden") self.assertFalse(result.startswith(".")) self.assertEqual(result, "hidden") def test_multiple_leading_dots_removed(self): """多个开头的点应该全部被移除.""" result = sanitize_filename("...hidden") self.assertFalse(result.startswith(".")) def test_empty_filename_default(self): """空文件名应该返回 unnamed.""" self.assertEqual(sanitize_filename(""), "unnamed") def test_special_chars_removed(self): """特殊字符应该被替换.""" result = sanitize_filename('file:"test|?*.mp4') self.assertNotIn("<", result) self.assertNotIn(">", result) self.assertNotIn(":", result) self.assertNotIn('"', result) self.assertNotIn("|", result) self.assertNotIn("?", result) self.assertNotIn("*", result) def test_chinese_filename_preserved(self): """中文文件名应该保留.""" result = sanitize_filename("视频素材.mp4") self.assertIn("视频素材", result) def test_long_filename_truncated(self): """超长文件名应该被截断.""" long_name = "a" * 300 + ".mp4" result = sanitize_filename(long_name) self.assertLessEqual(len(result), 255) self.assertTrue(result.endswith(".mp4")) class TestAllowedDirs(unittest.TestCase): """允许目录配置测试.""" def test_get_allowed_dirs_returns_list(self): """get_allowed_local_dirs 应该返回列表.""" dirs = get_allowed_local_dirs() self.assertIsInstance(dirs, list) def test_is_in_allowed_dirs_tmp(self): """/tmp 应该在默认允许目录内.""" self.assertTrue(is_in_allowed_dirs("/tmp/test.mp4")) def test_is_path_safe_convenience(self): """is_path_safe 便捷函数应该正常工作.""" with tempfile.TemporaryDirectory() as tmpdir: self.assertTrue(is_path_safe("test.mp4", tmpdir)) self.assertFalse(is_path_safe("../etc/passwd", tmpdir)) if __name__ == "__main__": unittest.main()