Files
ss-tools/backend/tests/services/git/test_git_base_coverage2.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

219 lines
11 KiB
Python

# #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 -> [Services.Base.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