test: 8 parallel agents — fix 150+ failures, add 30+ test files. schemas/models 99-100%, core/client_registry ~98%, maintenance 95-98%, git edges 97%, translate 98-100%, dashboard routes 95-96%, dataset_review 100%
This commit is contained in:
264
backend/tests/services/git/test_git_base_edge.py
Normal file
264
backend/tests/services/git/test_git_base_edge.py
Normal file
@@ -0,0 +1,264 @@
|
||||
# #region Test.Git.Base.Edge [C:3] [TYPE Module] [SEMANTICS test, git, base, repo, migrate, coverage]
|
||||
# @BRIEF Additional edge case tests for GitServiceBase — repo path resolution, migration, delete_repo edge paths.
|
||||
# @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 fastapi import HTTPException
|
||||
|
||||
from src.services.git._base import GitServiceBase
|
||||
|
||||
|
||||
class TestResolveBasePath:
|
||||
def test_custom_base_path_returns_direct(self):
|
||||
"""Custom base_path (not 'git_repos') returns path directly."""
|
||||
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
||||
patch.object(GitServiceBase, '_resolve_base_path', return_value="/custom/path"):
|
||||
svc = GitServiceBase(base_path="/custom/path")
|
||||
# Already resolved in __init__
|
||||
assert svc.base_path == "/custom/path"
|
||||
|
||||
def test_fallback_on_db_error(self):
|
||||
"""DB error during resolve → fallback to default."""
|
||||
with patch("src.services.git._base.SessionLocal", side_effect=Exception("DB error")):
|
||||
with patch.object(GitServiceBase, '_ensure_base_path_exists'):
|
||||
svc = GitServiceBase(base_path="git_repos")
|
||||
assert svc.base_path is not None
|
||||
|
||||
|
||||
class TestGetRepoPathDBPath:
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_path_exists_returned(self):
|
||||
"""DB has local_path and it exists → return it."""
|
||||
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_repo = MagicMock()
|
||||
mock_db_repo.local_path = "/tmp/existing/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):
|
||||
result = await svc._get_repo_path(1, "my-key")
|
||||
assert result == "/tmp/existing/repo"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_path_legacy_migration(self):
|
||||
"""DB path starts with legacy base → migrate."""
|
||||
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, return_value="/tmp/new-base/my-key"):
|
||||
result = await svc._get_repo_path(1, "my-key")
|
||||
assert result == "/tmp/new-base/my-key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_query_exception(self):
|
||||
"""Exception in DB query → fallback to path resolution."""
|
||||
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", side_effect=Exception("DB down")):
|
||||
result = await svc._get_repo_path(1, "my-key")
|
||||
assert result == "/tmp/base/my-key"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_id_path_migration(self):
|
||||
"""Legacy ID path exists → migrate to new path."""
|
||||
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", side_effect=lambda p: p == "/tmp/legacy/1"), \
|
||||
patch.object(svc, '_migrate_repo_directory', new_callable=AsyncMock, return_value="/tmp/base/dashboard"):
|
||||
result = await svc._get_repo_path(1)
|
||||
assert result == "/tmp/base/dashboard"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_target_path_exists_updates_db(self):
|
||||
"""Target path already exists → update DB record."""
|
||||
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()
|
||||
|
||||
|
||||
class TestDeleteRepoEdge:
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_repo_file_not_dir(self):
|
||||
"""Repo path exists but is a file → use os.remove."""
|
||||
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("os.remove", new_callable=MagicMock) as mock_remove, \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
|
||||
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
|
||||
with pytest.raises(HTTPException, match="404"):
|
||||
await svc.delete_repo(1)
|
||||
mock_remove.assert_called_once_with("/tmp/repo-file")
|
||||
finally:
|
||||
gb_mod.SessionLocal = orig_sl
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_repo_db_error_rollback(self):
|
||||
"""DB error during delete → rollback and HTTP 500."""
|
||||
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"), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.isdir", return_value=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
|
||||
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_repo = MagicMock()
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
|
||||
mock_db.delete.side_effect = Exception("DB error")
|
||||
gb_mod.run_blocking = AsyncMock()
|
||||
|
||||
with pytest.raises(HTTPException, match="500"):
|
||||
await svc.delete_repo(1)
|
||||
mock_db.rollback.assert_called_once()
|
||||
finally:
|
||||
gb_mod.SessionLocal = orig_sl
|
||||
|
||||
|
||||
class TestMigrateRepoDirectoryEdges:
|
||||
@pytest.mark.asyncio
|
||||
async def test_os_replace_fallback_to_shutil(self):
|
||||
"""OSError on os.replace → falls back to shutil.move."""
|
||||
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.object(svc, '_update_repo_local_path'), \
|
||||
patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_blocking:
|
||||
# Make first os.replace fail, second shutil.move succeed
|
||||
mock_blocking.side_effect = [None, OSError("rename failed"), None]
|
||||
result = await svc._migrate_repo_directory(1, "/source", "/target")
|
||||
assert result == "/target"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_existing_target_keeps_source(self):
|
||||
"""Target already exists → return source."""
|
||||
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=True):
|
||||
result = await svc._migrate_repo_directory(1, "/source", "/target")
|
||||
assert result == "/source"
|
||||
|
||||
|
||||
class TestInitRepoEdge:
|
||||
@pytest.mark.asyncio
|
||||
async def test_existing_path_is_not_git_repo_recreates(self):
|
||||
"""Path exists but is not a valid Git repo → recreate."""
|
||||
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.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch("src.services.git._base.run_blocking") as mock_blocking, \
|
||||
patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
|
||||
patch.object(svc, "_ensure_gitflow_branches", create=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
|
||||
# First call (Repo(repo_path)) raises InvalidGitRepositoryError
|
||||
mock_blocking.side_effect = [Exception("InvalidGitRepositoryError"), None, None]
|
||||
mock_clone.return_value = MagicMock()
|
||||
result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
|
||||
assert result is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stale_path_unlink_fallback(self):
|
||||
"""stale_path.exists after rmtree → tries unlink."""
|
||||
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.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch("src.services.git._base.run_blocking") as mock_blocking, \
|
||||
patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
|
||||
patch.object(svc, "_ensure_gitflow_branches", create=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()), \
|
||||
patch("pathlib.Path.exists", return_value=True):
|
||||
|
||||
mock_blocking.side_effect = [
|
||||
Exception("InvalidGitRepositoryError"), # Repo() call
|
||||
None, # shutil.rmtree
|
||||
None, # stale_path.unlink
|
||||
MagicMock(), # Repo from clone
|
||||
]
|
||||
mock_clone.return_value = MagicMock()
|
||||
result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
|
||||
assert result is not None
|
||||
|
||||
|
||||
class TestClosedService:
|
||||
def test_closed_service_blocks_locked(self):
|
||||
"""Closed service raises RuntimeError in _locked."""
|
||||
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
||||
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
||||
svc = GitServiceBase(base_path="/tmp/test")
|
||||
svc._closed = True
|
||||
with pytest.raises(RuntimeError, match="closed"):
|
||||
with svc._locked(1):
|
||||
pass
|
||||
# #endregion Test.Git.Base.Edge
|
||||
Reference in New Issue
Block a user