# #region Test.Git.Branch.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, branch, coverage, error, edge] # @BRIEF Additional edge coverage for GitServiceBranchMixin — origin fetch failure, push failure, ref exception, create_head failure, stderr parsing. # @RELATION BINDS_TO -> [GitServiceBranchMixin] 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 from src.services.git._branch import GitServiceBranchMixin class TestableBranch(GitServiceBranchMixin): """Minimal test wrapper matching test_git_branch.py pattern.""" def __init__(self, mock_repo=None): self._mock_repo = mock_repo or MagicMock() async def get_repo(self, dashboard_id): return self._mock_repo def _locked(self, dashboard_id): @contextlib.contextmanager def _lock(): yield return _lock() class TestEnsureGitflowBranchesCoverage: """Cover origin.fetch failure and push failure paths.""" def test_fetch_failure_logged(self): """origin.fetch() raises → fetch error logged, push still attempted.""" repo = MagicMock() main_head = MagicMock() main_head.name = "main" main_head.commit = MagicMock() repo.heads = [main_head] repo.head.commit = MagicMock() origin = MagicMock() origin.fetch.side_effect = Exception("fetch failed") repo.remote.return_value = origin svc = TestableBranch() svc._ensure_gitflow_branches(repo, 1) # Should have pushed branches (dev, preprod) assert origin.push.call_count >= 2 def test_push_failure_raises_500(self): """Branch push to origin fails → HTTPException 500.""" repo = MagicMock() main_head = MagicMock() main_head.name = "main" main_head.commit = MagicMock() repo.heads = [main_head] repo.head.commit = MagicMock() origin = MagicMock() origin.push.side_effect = Exception("push denied") repo.remote.return_value = origin svc = TestableBranch() with pytest.raises(HTTPException, match="Failed to create default branch"): svc._ensure_gitflow_branches(repo, 1) class TestListBranchesCoverage: """Cover ref processing exception and active-branch-not-listed paths.""" @pytest.mark.asyncio async def test_ref_processing_exception(self): """Ref processing raises → skipped with log.""" repo = MagicMock() bad_ref = MagicMock() bad_ref.name = "refs/heads/dev" type(bad_ref).commit = PropertyMock(side_effect=Exception("no commit")) repo.refs = [bad_ref] type(repo.active_branch).name = PropertyMock(return_value="dev") svc = TestableBranch(repo) result = await svc.list_branches(1) # 'dev' should appear from active branch fallback assert any(b["name"] == "dev" for b in result) @pytest.mark.asyncio async def test_active_branch_not_in_list(self): """Active branch not in refs list → explicitly added.""" repo = MagicMock() repo.refs = [] type(repo.active_branch).name = PropertyMock(return_value="main") svc = TestableBranch(repo) result = await svc.list_branches(1) assert any(b["name"] == "main" for b in result) class TestCreateBranchCoverage: """Cover create_head failure path.""" @pytest.mark.asyncio async def test_create_head_failure(self): """create_head raises → exception propagates.""" repo = MagicMock() repo.heads = [MagicMock()] repo.remotes = [MagicMock()] repo.commit.return_value = MagicMock() repo.create_head.side_effect = Exception("name conflict") svc = TestableBranch(repo) with pytest.raises(Exception, match="name conflict"): await svc.create_branch(1, "feature", "main") class TestCheckoutBranchCoverage: """Cover stderr file-parsing path.""" @pytest.mark.asyncio async def test_checkout_stderr_parses_files(self): """stderr with file paths → GIT_CHECKOUT_LOCAL_CHANGES raised.""" repo = MagicMock() error = GitCommandError("checkout", "error") error.stderr = ( "error: Your local changes to the following files would be overwritten by checkout:\n" "\tconfig.yaml\n" "\tsrc/app.py\n" ) repo.git.checkout.side_effect = error svc = TestableBranch(repo) with pytest.raises(HTTPException) as exc_info: await svc.checkout_branch(1, "other") detail = exc_info.value.detail assert detail["error_code"] == "GIT_CHECKOUT_LOCAL_CHANGES" # #endregion Test.Git.Branch.Coverage