# #region Test.Git.Base.Coverage2 [C:3] [TYPE Module] [SEMANTICS test, git, base, coverage, resolve, migrate] # @BRIEF Additional coverage for GitServiceBase uncovered lines: # _resolve_base_path (lines 139, 151), _migrate_repo_directory (lines 212-221), # _get_repo_path (lines 258, 264, 266), delete_repo (lines 320, 334). # @RELATION BINDS_TO -> [GitServiceBase] import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) import os from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.services.git._base import GitServiceBase class TestResolveBasePathCoverage2: """Cover _resolve_base_path lines 139 and 151.""" # Line 139: if base_path != "git_repos" → return fallback_path # To hit this we actually call the real _resolve_base_path via __init__ # with a non-git_repos base_path. def test_line139_custom_base_path_returns_fallback(self): """base_path != 'git_repos' → returns fallback_path (line 139).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="/custom/path") assert "custom" in svc.base_path # Line 151: if not root_path → return fallback_path # When storage config has empty root_path def test_line151_empty_root_path_returns_fallback(self): """storage root_path is empty → falls back to default (line 151).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'), \ patch("src.services.git._base.SessionLocal") as mock_sl: mock_db = MagicMock() mock_sl.return_value = mock_db config = MagicMock() config.payload = { "settings": { "storage": { "root_path": "", "repo_path": "", } } } mock_db.query.return_value.filter.return_value.first.return_value = config svc = GitServiceBase(base_path="git_repos") # With empty root_path, line 150 is True → line 151 returns fallback assert svc.base_path is not None class TestMigrateRepoDirectoryCoverage: """Cover _migrate_repo_directory lines 212-221.""" @pytest.mark.asyncio async def test_migrate_full_path_mkdir_and_replace(self): """Full migration: mkdir parent, os.replace, update local path, log, return target (lines 212-221).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'), \ patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"): svc = GitServiceBase(base_path="/tmp/test") with patch("os.path.exists", return_value=False), \ patch("os.path.abspath", side_effect=lambda x: x), \ patch.object(svc, '_update_repo_local_path') as mock_update, \ patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking, \ patch("src.services.git._base.logger.info") as mock_log: # mkdir succeeds, os.replace succeeds mock_blocking.side_effect = [None, None] result = await svc._migrate_repo_directory(1, "/source/path", "/target/path") assert result == "/target/path" mock_update.assert_called_once_with(1, "/target/path") mock_log.assert_called_once() @pytest.mark.asyncio async def test_migrate_os_replace_fallback_to_move(self): """os.replace OSError → shutil.move fallback (line 215-216).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'), \ patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"): svc = GitServiceBase(base_path="/tmp/test") with patch("os.path.exists", return_value=False), \ patch("os.path.abspath", side_effect=lambda x: x), \ patch.object(svc, '_update_repo_local_path'), \ patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking: # mkdir succeeds, os.replace raises OSError, shutil.move succeeds mock_blocking.side_effect = [None, OSError("rename failed"), None] result = await svc._migrate_repo_directory(1, "/source/path", "/target/path") assert result == "/target/path" class TestGetRepoPathCoverage2: """Cover _get_repo_path lines 258, 264, 266.""" # Line 258: DB path starts with legacy and base_path != legacy → migrate @pytest.mark.asyncio async def test_line258_db_legacy_path_migration(self): """DB local_path under legacy base with different base_path → migrate (line 258).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="git_repos") svc.base_path = "/tmp/new-base" svc.legacy_base_path = "/tmp/legacy" svc._uses_default_base_path = True with patch("src.services.git._base.SessionLocal") as mock_sl: mock_db = MagicMock() mock_sl.return_value = mock_db mock_db_repo = MagicMock() mock_db_repo.local_path = "/tmp/legacy/repo" mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo with patch("os.path.exists", return_value=True), \ patch("os.path.abspath", side_effect=lambda x: x), \ patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock) as mock_migrate: mock_migrate.return_value = "/tmp/new-base/my-key" result = await svc._get_repo_path(1, "my-key") assert result == "/tmp/new-base/my-key" mock_migrate.assert_called_once() # Line 264: legacy ID path exists → migrate # The normalized key for dashboard_id=1 is "1" (via _normalize_repo_key) @pytest.mark.asyncio async def test_line264_legacy_id_path_migration(self): """Legacy ID path exists, target doesn't → migrate (line 264).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="git_repos") svc.base_path = "/tmp/new-base" svc.legacy_base_path = "/tmp/legacy" svc._uses_default_base_path = True with patch("src.services.git._base.SessionLocal") as mock_sl: mock_db = MagicMock() mock_sl.return_value = mock_db mock_db.query.return_value.filter.return_value.first.return_value = None legacy_path = "/tmp/legacy/1" # normalized key for dashboard_id=1 (no repo_key) is "1" target_path = "/tmp/new-base/1" with patch("os.path.exists", side_effect=lambda p: p == legacy_path), \ patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock) as mock_migrate: mock_migrate.return_value = target_path result = await svc._get_repo_path(1) assert result == target_path mock_migrate.assert_called_once_with(1, legacy_path, target_path) # Line 266: target path exists → _update_repo_local_path called @pytest.mark.asyncio async def test_line266_target_path_exists_updates_db(self): """Target path exists → updates DB (line 266).""" with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="git_repos") svc.base_path = "/tmp/base" svc.legacy_base_path = "/tmp/legacy" svc._uses_default_base_path = True with patch("src.services.git._base.SessionLocal") as mock_sl: mock_db = MagicMock() mock_sl.return_value = mock_db mock_db.query.return_value.filter.return_value.first.return_value = None with patch("os.path.exists", return_value=True), \ patch.object(svc, '_update_repo_local_path') as mock_update: result = await svc._get_repo_path(1, "my-key") assert result == "/tmp/base/my-key" mock_update.assert_called_once_with(1, "/tmp/base/my-key") class TestDeleteRepoCoverage2: """Cover delete_repo lines 320, 334.""" # Line 320: repo path is a file → os.remove @pytest.mark.asyncio async def test_line320_delete_file_path(self): """Repo path exists and is a file → os.remove (line 320).""" import contextlib with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="git_repos") svc.base_path = "/tmp/base" with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo-file"), \ patch("os.path.exists", return_value=True), \ patch("os.path.isdir", return_value=False), \ patch.object(svc, '_locked', return_value=contextlib.nullcontext()): import src.services.git._base as gb_mod orig_sl = gb_mod.SessionLocal try: mock_db = MagicMock() gb_mod.SessionLocal = MagicMock(return_value=mock_db) mock_db.query.return_value.filter.return_value.first.return_value = None gb_mod.run_blocking = AsyncMock() await svc.delete_repo(1) # os.remove should have been called (line 320) finally: gb_mod.SessionLocal = orig_sl # Line 334: removed_files=True but no DB record → early return @pytest.mark.asyncio async def test_line334_removed_no_db_record_returns(self): """Files removed, no DB record → early return (line 334).""" import contextlib with patch.object(GitServiceBase, '_ensure_base_path_exists'): svc = GitServiceBase(base_path="git_repos") svc.base_path = "/tmp/base" with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo-dir"), \ patch("os.path.exists", return_value=True), \ patch("os.path.isdir", return_value=True), \ patch.object(svc, '_locked', return_value=contextlib.nullcontext()): import src.services.git._base as gb_mod orig_sl = gb_mod.SessionLocal orig_blocking = gb_mod.run_blocking try: mock_db = MagicMock() gb_mod.SessionLocal = MagicMock(return_value=mock_db) mock_db.query.return_value.filter.return_value.first.return_value = None gb_mod.run_blocking = AsyncMock() result = await svc.delete_repo(1) assert result is None finally: gb_mod.SessionLocal = orig_sl gb_mod.run_blocking = orig_blocking # #endregion Test.Git.Base.Coverage2