# #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