import os import tempfile import pytest from pathguard import safe_join, PathTraversalError def test_basic_join(): with tempfile.TemporaryDirectory() as d: out = safe_join(d, "a", "b.txt") assert out == os.path.join(os.path.realpath(d), "a", "b.txt") def test_dotdot_escape_rejected(): with tempfile.TemporaryDirectory() as d: with pytest.raises(PathTraversalError): safe_join(d, "..", "etc", "passwd") def test_absolute_part_rejected(): with tempfile.TemporaryDirectory() as d: with pytest.raises(PathTraversalError): safe_join(d, "/etc/passwd") def test_symlink_escape_rejected(): with tempfile.TemporaryDirectory() as d: base = os.path.join(d, "base") os.makedirs(base) target = os.path.join(d, "secret.txt") with open(target, "w") as f: f.write("secret") link = os.path.join(base, "link") os.symlink(target, link) # reading through link to outside must be rejected when resolved? # safe_join resolves; link itself is inside base by name but points outside. # Our function resolves symlinks and checks containment -> should reject. with pytest.raises(PathTraversalError): safe_join(base, "link") def test_valid_nested(): with tempfile.TemporaryDirectory() as d: sub = os.path.join(d, "sub") os.makedirs(sub) out = safe_join(d, "sub", "file.txt") assert out.startswith(os.path.realpath(d) + os.sep) def test_empty_parts_ignored(): with tempfile.TemporaryDirectory() as d: out = safe_join(d, "", None, "x") assert out.endswith("x") def test_nul_rejected(): with tempfile.TemporaryDirectory() as d: with pytest.raises(PathTraversalError): safe_join(d, "a\x00b") def test_base_not_dir(): with tempfile.TemporaryDirectory() as d: f = os.path.join(d, "f") open(f, "w").close() with pytest.raises(PathTraversalError): safe_join(f, "x")