"""Tests for FileManager (backend/managers/file_manager.py).""" import sys from pathlib import Path import pytest from unittest.mock import patch sys.path.insert(0, str(Path(__file__).parent.parent)) from backend.managers.file_manager import FileManager # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest.fixture(autouse=True) def mock_streamlit(): """Suppress all st.error / st.warning calls — they require a running Streamlit app.""" with patch("backend.managers.file_manager.st"): yield @pytest.fixture def fm(tmp_path): """Return a FileManager whose workspace is an isolated pytest temp directory.""" return FileManager(base_path=tmp_path) # ── create_folder ───────────────────────────────────────────────────────────── class TestCreateFolder: """Tests for create_folder(): name validation, path-traversal protection, and nested creation.""" def test_creates_folder_successfully(self, fm, tmp_path): result = fm.create_folder("", "myfolder") assert result is True assert (tmp_path / "myfolder").is_dir() def test_empty_name_returns_false(self, fm): assert fm.create_folder("", "") is False def test_slash_in_name_returns_false(self, fm): assert fm.create_folder("", "a/b") is False def test_backslash_in_name_returns_false(self, fm): assert fm.create_folder("", "a\\b") is False def test_duplicate_folder_returns_false(self, fm, tmp_path): (tmp_path / "existing").mkdir() assert fm.create_folder("", "existing") is False def test_path_traversal_returns_false(self, fm): assert fm.create_folder("../../", "evil") is False def test_nested_folder_created_inside_base(self, fm, tmp_path): (tmp_path / "sub").mkdir() result = fm.create_folder("sub", "child") assert result is True assert (tmp_path / "sub" / "child").is_dir() # ── create_file ─────────────────────────────────────────────────────────────── class TestCreateFile: """Tests for create_file(): name validation, auto .txt extension, and path-traversal protection.""" def test_creates_file_successfully(self, fm, tmp_path): result = fm.create_file("", "test.py") assert result is True assert (tmp_path / "test.py").is_file() def test_empty_name_returns_false(self, fm): assert fm.create_file("", "") is False def test_whitespace_only_name_returns_false(self, fm): assert fm.create_file("", " ") is False def test_no_extension_defaults_to_txt(self, fm, tmp_path): fm.create_file("", "notes") assert (tmp_path / "notes.txt").is_file() def test_duplicate_file_returns_false(self, fm, tmp_path): (tmp_path / "existing.py").touch() assert fm.create_file("", "existing.py") is False def test_path_traversal_returns_false(self, fm): assert fm.create_file("../../", "evil.py") is False # ── read_file ───────────────────────────────────────────────────────────────── class TestReadFile: """Tests for read_file(): accepts an absolute Path, validates workspace boundary, returns content or "".""" def test_reads_file_content(self, fm, tmp_path): f = tmp_path / "hello.py" f.write_text("print('hello')") assert fm.read_file(f) == "print('hello')" def test_nonexistent_file_returns_empty_string(self, fm, tmp_path): assert fm.read_file(tmp_path / "ghost.py") == "" def test_directory_path_returns_empty_string(self, fm, tmp_path): sub = tmp_path / "subdir" sub.mkdir() assert fm.read_file(sub) == "" def test_file_outside_workspace_returns_empty_string(self, fm, tmp_path): # tmp_path.parent is a shared pytest temp dir, so we clean up to avoid # leaving stray files that could affect other tests. outside = tmp_path.parent / "outside.py" outside.write_text("secret") result = fm.read_file(outside) assert result == "" outside.unlink() # ── save_file ───────────────────────────────────────────────────────────────── class TestSaveFile: """Tests for save_file(): accepts an absolute path string, overwrites content, and blocks path traversal.""" def test_saves_content_to_file(self, fm, tmp_path): f = tmp_path / "output.py" f.touch() result = fm.save_file(str(f), "x = 1") assert result is True assert f.read_text() == "x = 1" def test_overwrites_existing_content(self, fm, tmp_path): f = tmp_path / "script.py" f.write_text("old content") fm.save_file(str(f), "new content") assert f.read_text() == "new content" def test_path_traversal_returns_false(self, fm, tmp_path): outside = str(tmp_path.parent / "evil.py") assert fm.save_file(outside, "bad") is False # ── rename_file ─────────────────────────────────────────────────────────────── class TestRenameFile: """Tests for rename_file(): renames by stem only — the original extension is always preserved.""" def test_renames_file_successfully(self, fm, tmp_path): (tmp_path / "old.py").touch() result = fm.rename_file("old.py", "new") assert result is True assert (tmp_path / "new.py").exists() assert not (tmp_path / "old.py").exists() def test_preserves_original_extension(self, fm, tmp_path): # Even if the caller passes a different extension (.txt), rename_file # silently replaces it with the original (.py) to prevent accidental type changes. (tmp_path / "script.py").touch() fm.rename_file("script.py", "renamed.txt") assert (tmp_path / "renamed.py").exists() def test_empty_new_name_returns_false(self, fm, tmp_path): (tmp_path / "file.py").touch() assert fm.rename_file("file.py", "") is False def test_nonexistent_file_returns_false(self, fm): assert fm.rename_file("ghost.py", "new_name") is False def test_path_traversal_returns_false(self, fm): assert fm.rename_file("../../evil.py", "new_name") is False # ── delete_file ─────────────────────────────────────────────────────────────── class TestDeleteFile: """Tests for delete_file(): accepts a relative path, validates workspace boundary, removes the file.""" def test_deletes_file_successfully(self, fm, tmp_path): f = tmp_path / "todelete.py" f.touch() result = fm.delete_file("todelete.py") assert result is True assert not f.exists() def test_nonexistent_file_returns_false(self, fm): assert fm.delete_file("ghost.py") is False def test_path_traversal_returns_false(self, fm): assert fm.delete_file("../../evil.py") is False # ── delete_folder ───────────────────────────────────────────────────────────── class TestDeleteFolder: """Tests for delete_folder(): recursively removes a folder and all its contents.""" def test_deletes_folder_and_contents(self, fm, tmp_path): sub = tmp_path / "todelete" sub.mkdir() (sub / "file.py").touch() result = fm.delete_folder("todelete") assert result is True assert not sub.exists() def test_nonexistent_folder_returns_false(self, fm): assert fm.delete_folder("ghost") is False def test_path_traversal_returns_false(self, fm): assert fm.delete_folder("../../") is False # ── get_file_tree ───────────────────────────────────────────────────────────── class TestGetFileTree: """Tests for get_file_tree(): returns a nested dict where files map to None and dirs map to dicts.""" def test_empty_workspace_returns_empty_dict(self, fm): assert fm.get_file_tree() == {} def test_file_is_represented_as_none(self, fm, tmp_path): (tmp_path / "main.py").touch() tree = fm.get_file_tree() assert tree["main.py"] is None def test_directory_is_represented_as_dict(self, fm, tmp_path): (tmp_path / "src").mkdir() tree = fm.get_file_tree() assert isinstance(tree["src"], dict) def test_nested_structure_is_correct(self, fm, tmp_path): (tmp_path / "src").mkdir() (tmp_path / "src" / "app.py").touch() tree = fm.get_file_tree() assert tree["src"]["app.py"] is None def test_entries_are_sorted(self, fm, tmp_path): (tmp_path / "z_file.py").touch() (tmp_path / "a_file.py").touch() keys = list(fm.get_file_tree().keys()) assert keys == sorted(keys)