import builtins import io import ipaddress import os from pathlib import Path import shutil import socket import pytest PROTECTED_ROOTS = [ Path("/md0/jdownloads"), Path("/md0/movies1"), Path("/md0/tv1"), ] PROTECTED_MATCHERS = [] for p in PROTECTED_ROOTS: PROTECTED_MATCHERS.append(p) try: PROTECTED_MATCHERS.append(p.resolve()) except Exception: pass def check_target(target): """Check if target path resolves into or is inside protected production directories.""" if target is None or isinstance(target, int): return try: if isinstance(target, bytes): t_str = os.fsdecode(target) else: t_str = str(target) p = Path(t_str) p_resolved = p.resolve() except Exception: return for root in PROTECTED_MATCHERS: try: if p_resolved == root or p_resolved.is_relative_to(root): raise RuntimeError( f"FILESYSTEM SAFETY TRAP: Forbidden write/delete operation targeting production path '{target}' in test execution!" ) except RuntimeError: raise except Exception: pass try: if p == root or p.is_relative_to(root): raise RuntimeError( f"FILESYSTEM SAFETY TRAP: Forbidden write/delete operation targeting production path '{target}' in test execution!" ) except RuntimeError: raise except Exception: pass @pytest.fixture(autouse=True, scope="session") def protect_production_filesystem(): """Active safety trap that monkeypatches filesystem modification operations in os, shutil, pathlib.Path, and open (for write/append/create modes). Inspects all target paths: if any path resolves into or is inside /md0/jdownloads, /md0/movies1, or /md0/tv1, raises RuntimeError. """ mp = pytest.MonkeyPatch() # 1. Builtins & IO open orig_builtin_open = builtins.open def safe_builtin_open(file, *args, **kwargs): mode = "r" if args: mode = args[0] elif "mode" in kwargs: mode = kwargs["mode"] if any(m in mode for m in ("w", "a", "x", "+")): check_target(file) return orig_builtin_open(file, *args, **kwargs) mp.setattr(builtins, "open", safe_builtin_open) mp.setattr(io, "open", safe_builtin_open) # 2. pathlib.Path methods orig_path_open = Path.open orig_path_write_text = Path.write_text orig_path_write_bytes = Path.write_bytes orig_path_unlink = Path.unlink orig_path_rmdir = Path.rmdir orig_path_mkdir = Path.mkdir orig_path_rename = Path.rename orig_path_replace = Path.replace orig_path_touch = Path.touch def safe_path_open(self, *args, **kwargs): mode = "r" if args: mode = args[0] elif "mode" in kwargs: mode = kwargs["mode"] if any(m in mode for m in ("w", "a", "x", "+")): check_target(self) return orig_path_open(self, *args, **kwargs) def safe_path_write_text(self, *args, **kwargs): check_target(self) return orig_path_write_text(self, *args, **kwargs) def safe_path_write_bytes(self, *args, **kwargs): check_target(self) return orig_path_write_bytes(self, *args, **kwargs) def safe_path_unlink(self, *args, **kwargs): check_target(self) return orig_path_unlink(self, *args, **kwargs) def safe_path_rmdir(self, *args, **kwargs): check_target(self) return orig_path_rmdir(self, *args, **kwargs) def safe_path_mkdir(self, *args, **kwargs): check_target(self) return orig_path_mkdir(self, *args, **kwargs) def safe_path_rename(self, target, *args, **kwargs): check_target(self) check_target(target) return orig_path_rename(self, target, *args, **kwargs) def safe_path_replace(self, target, *args, **kwargs): check_target(self) check_target(target) return orig_path_replace(self, target, *args, **kwargs) def safe_path_touch(self, *args, **kwargs): check_target(self) return orig_path_touch(self, *args, **kwargs) mp.setattr(Path, "open", safe_path_open) mp.setattr(Path, "write_text", safe_path_write_text) mp.setattr(Path, "write_bytes", safe_path_write_bytes) mp.setattr(Path, "unlink", safe_path_unlink) mp.setattr(Path, "rmdir", safe_path_rmdir) mp.setattr(Path, "mkdir", safe_path_mkdir) mp.setattr(Path, "rename", safe_path_rename) mp.setattr(Path, "replace", safe_path_replace) mp.setattr(Path, "touch", safe_path_touch) # 3. os functions orig_os_remove = os.remove orig_os_unlink = os.unlink orig_os_rmdir = os.rmdir orig_os_mkdir = os.mkdir orig_os_makedirs = os.makedirs orig_os_rename = os.rename orig_os_replace = os.replace orig_os_open = os.open orig_os_truncate = os.truncate def safe_os_remove(path, *args, **kwargs): check_target(path) return orig_os_remove(path, *args, **kwargs) def safe_os_unlink(path, *args, **kwargs): check_target(path) return orig_os_unlink(path, *args, **kwargs) def safe_os_rmdir(path, *args, **kwargs): check_target(path) return orig_os_rmdir(path, *args, **kwargs) def safe_os_mkdir(path, *args, **kwargs): check_target(path) return orig_os_mkdir(path, *args, **kwargs) def safe_os_makedirs(name, *args, **kwargs): check_target(name) return orig_os_makedirs(name, *args, **kwargs) def safe_os_rename(src, dst, *args, **kwargs): check_target(src) check_target(dst) return orig_os_rename(src, dst, *args, **kwargs) def safe_os_replace(src, dst, *args, **kwargs): check_target(src) check_target(dst) return orig_os_replace(src, dst, *args, **kwargs) def safe_os_open(path, flags, *args, **kwargs): if flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND): check_target(path) return orig_os_open(path, flags, *args, **kwargs) def safe_os_truncate(path, *args, **kwargs): check_target(path) return orig_os_truncate(path, *args, **kwargs) mp.setattr(os, "remove", safe_os_remove) mp.setattr(os, "unlink", safe_os_unlink) mp.setattr(os, "rmdir", safe_os_rmdir) mp.setattr(os, "mkdir", safe_os_mkdir) mp.setattr(os, "makedirs", safe_os_makedirs) mp.setattr(os, "rename", safe_os_rename) mp.setattr(os, "replace", safe_os_replace) mp.setattr(os, "open", safe_os_open) mp.setattr(os, "truncate", safe_os_truncate) # 4. shutil functions orig_shutil_rmtree = shutil.rmtree orig_shutil_move = shutil.move orig_shutil_copy = shutil.copy orig_shutil_copy2 = shutil.copy2 orig_shutil_copyfile = shutil.copyfile orig_shutil_copytree = shutil.copytree def safe_shutil_rmtree(path, *args, **kwargs): check_target(path) return orig_shutil_rmtree(path, *args, **kwargs) def safe_shutil_move(src, dst, *args, **kwargs): check_target(src) check_target(dst) return orig_shutil_move(src, dst, *args, **kwargs) def safe_shutil_copy(src, dst, *args, **kwargs): check_target(dst) return orig_shutil_copy(src, dst, *args, **kwargs) def safe_shutil_copy2(src, dst, *args, **kwargs): check_target(dst) return orig_shutil_copy2(src, dst, *args, **kwargs) def safe_shutil_copyfile(src, dst, *args, **kwargs): check_target(dst) return orig_shutil_copyfile(src, dst, *args, **kwargs) def safe_shutil_copytree(src, dst, *args, **kwargs): check_target(dst) return orig_shutil_copytree(src, dst, *args, **kwargs) mp.setattr(shutil, "rmtree", safe_shutil_rmtree) mp.setattr(shutil, "move", safe_shutil_move) mp.setattr(shutil, "copy", safe_shutil_copy) mp.setattr(shutil, "copy2", safe_shutil_copy2) mp.setattr(shutil, "copyfile", safe_shutil_copyfile) mp.setattr(shutil, "copytree", safe_shutil_copytree) yield mp.undo() @pytest.fixture(autouse=True, scope="function") def isolate_test_environment(): """Saves os.environ before test and restores it after. Strips existing CONFIDENCE_THRESHOLD, DOWNLOADS_DIR, MOVIES_DIR, SHOWS_DIR, ANIME_DIR, SOURCE_DIR, TV_DIR, DRY_RUN, ACTION, and MEDIA_SORTER_* variables so Settings() initializes with clean test defaults and prevents cross-test contamination. """ saved_env = dict(os.environ) keys_to_strip = [ "CONFIDENCE_THRESHOLD", "DOWNLOADS_DIR", "MOVIES_DIR", "SHOWS_DIR", "ANIME_DIR", "SOURCE_DIR", "TV_DIR", "DRY_RUN", "ACTION", ] for key in keys_to_strip: os.environ.pop(key, None) for key in list(os.environ.keys()): if key.startswith("MEDIA_SORTER_"): os.environ.pop(key, None) try: yield finally: os.environ.clear() os.environ.update(saved_env) @pytest.fixture(autouse=True, scope="session") def block_external_network(): """Monkeypatches socket.socket.connect to prevent outbound internet network requests during tests (allow loopback/localhost and unix domain sockets). """ mp = pytest.MonkeyPatch() orig_connect = socket.socket.connect def safe_connect(self, address): # Allow Unix domain sockets if hasattr(socket, "AF_UNIX") and self.family == socket.AF_UNIX: return orig_connect(self, address) if isinstance(address, (str, bytes)): return orig_connect(self, address) # For INET/INET6 sockets, address is (host, port, ...) if isinstance(address, tuple) and len(address) >= 1: host = address[0] if host in ("localhost", "127.0.0.1", "::1", "0.0.0.0"): return orig_connect(self, address) try: ip = ipaddress.ip_address(host) if ip.is_loopback: return orig_connect(self, address) except ValueError: pass raise RuntimeError( f"NETWORK ACCESS TRAP: Outbound network connection to {address} blocked during tests!" ) mp.setattr(socket.socket, "connect", safe_connect) yield mp.undo()