Files
ss-tools/backend/tests/services/git/test_git_sync_coverage.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

183 lines
8.0 KiB
Python

# #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 -> [Services.Sync.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_rejects_push_for_binding_on_another_git_server(self, mixin):
"""Push must never silently retarget a repository by replacing its host."""
from src.models.git import GitRepository
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
origin.urls = ["https://try.gitea.io/e2e-test/dashboard-1"]
repo.remote.return_value = origin
binding = GitRepository(
dashboard_id=1,
config_id="config-1",
remote_url="https://try.gitea.io/e2e-test/dashboard-1",
local_path="/tmp/dashboard-1",
)
config = MagicMock(url="https://git.bebesh.ru")
repo_query = MagicMock()
repo_query.filter.return_value.first.return_value = binding
config_query = MagicMock()
config_query.filter.return_value.first.return_value = config
with patch.object(mixin, "get_repo", new_callable=AsyncMock, return_value=repo, create=True), \
patch("src.services.git._sync.SessionLocal") as session_local:
session = MagicMock()
session.query.side_effect = [repo_query, config_query]
session_local.return_value = session
with pytest.raises(HTTPException, match="another Git server") as error:
await mixin.push_changes(1)
assert error.value.status_code == 409
origin.push.assert_not_called()
@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