test: 7 agents — plugins ~158 tests, extractor 98-100%, dashboard routes 99-100%, git services 94-100%, services 94-100%, dataset_review 100%. Fix test_api_key_routes.py sys.modules pollution
This commit is contained in:
151
backend/tests/services/git/test_git_sync_coverage.py
Normal file
151
backend/tests/services/git/test_git_sync_coverage.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# #region Test.Git.Sync.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, sync, coverage, push, pull, error]
|
||||
# @BRIEF Additional edge- and error-path coverage for GitServiceSyncMixin — push/pull diagnostic and origin-url exception branches.
|
||||
# @RELATION BINDS_TO -> [GitServiceSyncMixin]
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from git.exc import GitCommandError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mixin():
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
m = GitServiceSyncMixin()
|
||||
m._locked = lambda x: contextlib.nullcontext()
|
||||
m._align_origin_host_with_config = MagicMock(return_value=None)
|
||||
return m
|
||||
|
||||
|
||||
class TestPushChangesCoverage:
|
||||
"""Cover remaining push_changes branches."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_origin_urls_exception(self, mixin):
|
||||
"""Getting origin URLs raises → empty list used."""
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
branch = MagicMock()
|
||||
branch.name = "dev"
|
||||
branch.tracking_branch.return_value = MagicMock()
|
||||
repo.active_branch = branch
|
||||
origin = MagicMock()
|
||||
origin.urls = ["https://origin.com/repo.git"]
|
||||
type(origin).urls = PropertyMock(side_effect=Exception("url error"))
|
||||
repo.remote.return_value = origin
|
||||
origin.push.return_value = []
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("src.services.git._sync.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
|
||||
await mixin.push_changes(1)
|
||||
origin.push.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_diag_error(self, mixin):
|
||||
"""DB diagnostics fails during push → error logged, push proceeds."""
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
branch = MagicMock()
|
||||
branch.name = "dev"
|
||||
branch.tracking_branch.return_value = MagicMock()
|
||||
repo.active_branch = branch
|
||||
origin = MagicMock()
|
||||
origin.urls = ["https://origin.com/repo.git"]
|
||||
repo.remote.return_value = origin
|
||||
origin.push.return_value = []
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("src.services.git._sync.SessionLocal", side_effect=Exception("DB down")):
|
||||
await mixin.push_changes(1)
|
||||
origin.push.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_second_origin_urls_exception(self, mixin):
|
||||
"""Second origin.urls read after realignment raises → empty list used."""
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
branch = MagicMock()
|
||||
branch.name = "dev"
|
||||
branch.tracking_branch.return_value = MagicMock()
|
||||
repo.active_branch = branch
|
||||
origin = MagicMock()
|
||||
repo.remote.return_value = origin
|
||||
origin.push.return_value = []
|
||||
# First read succeeds, second fails
|
||||
origin.urls = ["https://origin.com/repo.git"]
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("src.services.git._sync.SessionLocal") as mock_sl, \
|
||||
patch.object(origin, 'urls', PropertyMock(side_effect=[["https://origin.com/repo.git"], Exception("urls error")])):
|
||||
mock_db = MagicMock()
|
||||
mock_sl.return_value = mock_db
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = None
|
||||
await mixin.push_changes(1)
|
||||
origin.push.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tracking_branch_exception(self, mixin):
|
||||
"""tracking_branch() raises Exception → treated as no tracking → set-upstream."""
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
branch = MagicMock()
|
||||
branch.name = "feature"
|
||||
branch.tracking_branch.side_effect = Exception("no tracking")
|
||||
repo.active_branch = branch
|
||||
origin = MagicMock()
|
||||
origin.urls = ["https://origin.com/repo.git"]
|
||||
repo.remote.return_value = origin
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("src.services.git._sync.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
|
||||
await mixin.push_changes(1)
|
||||
repo.git.push.assert_called_with("--set-upstream", "origin", "feature:feature")
|
||||
|
||||
|
||||
class TestPullChangesCoverage:
|
||||
"""Cover remaining pull_changes branches."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_origin_urls_exception(self, mixin):
|
||||
"""Getting origin.urls raises during pull → empty list."""
|
||||
repo = MagicMock()
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.active_branch.name = "dev"
|
||||
origin = MagicMock()
|
||||
type(origin).urls = PropertyMock(side_effect=Exception("urls error"))
|
||||
repo.remote.return_value = origin
|
||||
remote_ref = MagicMock()
|
||||
remote_ref.name = "origin/dev"
|
||||
repo.refs = [remote_ref]
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=False):
|
||||
await mixin.pull_changes(1)
|
||||
origin.fetch.assert_called_once_with(prune=True)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_git_command_error_non_conflict(self, mixin):
|
||||
"""GitCommandError during pull that is NOT conflict-related → HTTP 500."""
|
||||
repo = MagicMock()
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.active_branch.name = "dev"
|
||||
origin = MagicMock()
|
||||
origin.urls = ["https://origin.com/repo.git"]
|
||||
repo.remote.return_value = origin
|
||||
remote_ref = MagicMock()
|
||||
remote_ref.name = "origin/dev"
|
||||
repo.refs = [remote_ref]
|
||||
repo.git.pull.side_effect = GitCommandError("pull", "fatal: Could not read from remote repository.")
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=False):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await mixin.pull_changes(1)
|
||||
assert exc_info.value.status_code == 500
|
||||
# #endregion Test.Git.Sync.Coverage
|
||||
Reference in New Issue
Block a user