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:
132
backend/tests/services/git/test_git_branch_coverage.py
Normal file
132
backend/tests/services/git/test_git_branch_coverage.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# #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
|
||||
Reference in New Issue
Block a user