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:
2026-06-15 18:30:05 +03:00
parent 3de67c258a
commit 51d90f58c1
40 changed files with 9629 additions and 4 deletions

View File

@@ -7,7 +7,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import contextlib
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import pytest
from fastapi import HTTPException
@@ -246,4 +246,193 @@ class TestPullChanges:
patch("os.path.exists", return_value=False):
with pytest.raises(HTTPException, match="500"):
await mixin.pull_changes(1)
class TestPushChangesEdge:
"""push_changes — diagnostic and origin.urls exception paths."""
@pytest.mark.asyncio
async def test_origin_urls_exception(self):
"""list(origin.urls) raises → caught, handled."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
mixin._align_origin_host_with_config = MagicMock(return_value=None)
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
# list(origin.urls) raises exception
type(origin).urls = PropertyMock(side_effect=Exception("urls error"))
repo.remote.return_value = origin
branch = MagicMock()
branch.name = "main"
branch.tracking_branch.return_value = None
repo.active_branch = branch
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
mock_db = MagicMock()
mock_sl_cls.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", "main:main")
@pytest.mark.asyncio
async def test_tracking_branch_exception(self):
"""tracking_branch() raises → handled as None."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
mixin._align_origin_host_with_config = MagicMock(return_value=None)
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
origin.urls = ["https://git.com/org/repo.git"]
repo.remote.return_value = origin
branch = MagicMock()
branch.name = "main"
branch.tracking_branch.side_effect = Exception("no tracking")
repo.active_branch = branch
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
mock_db = MagicMock()
mock_sl_cls.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", "main:main")
@pytest.mark.asyncio
async def test_push_generic_git_command_error_no_match(self):
"""GitCommandError that is not non-fast-forward → HTTPException 500."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
mixin._align_origin_host_with_config = MagicMock(return_value=None)
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
origin.urls = ["https://git.com/org/repo.git"]
repo.remote.return_value = origin
branch = MagicMock()
branch.name = "main"
branch.tracking_branch.return_value = None
repo.active_branch = branch
repo.git.push.side_effect = GitCommandError("push", "unknown git error")
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
mock_db = MagicMock()
mock_sl_cls.return_value = mock_db
mock_db.query.return_value.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as exc_info:
await mixin.push_changes(1)
assert exc_info.value.status_code == 500
@pytest.mark.asyncio
async def test_push_push_info_raising_exception(self):
"""origin.push raises generic Exception → HTTPException 500."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
mixin._align_origin_host_with_config = MagicMock(return_value=None)
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
repo.remote.return_value = origin
branch = MagicMock()
branch.name = "main"
branch.tracking_branch.return_value = None
repo.active_branch = branch
# Force the else branch (tracking_branch is None → repo.git.push)
repo.git.push.return_value = None # won't be called because tracking_branch is None
repo.git.push.side_effect = Exception("unexpected failure")
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
mock_db = MagicMock()
mock_sl_cls.return_value = mock_db
mock_db.query.return_value.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as exc_info:
await mixin.push_changes(1)
assert exc_info.value.status_code == 500
@pytest.mark.asyncio
async def test_push_with_origin_push_generic_error(self):
"""origin.push() generic exception → HTTPException 500."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
mixin._align_origin_host_with_config = MagicMock(return_value=None)
repo = MagicMock()
repo.heads = [MagicMock()]
origin = MagicMock()
origin.urls = ["https://git.com/org/repo.git"]
repo.remote.return_value = origin
branch = MagicMock()
branch.name = "main"
branch.tracking_branch.return_value = MagicMock() # has tracking → uses origin.push
repo.active_branch = branch
origin.push.side_effect = Exception("generic push error")
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
mock_db = MagicMock()
mock_sl_cls.return_value = mock_db
mock_db.query.return_value.filter.return_value.first.return_value = None
with pytest.raises(HTTPException) as exc_info:
await mixin.push_changes(1)
assert exc_info.value.status_code == 500
class TestPullChangesEdge:
"""pull_changes — remaining edge paths."""
@pytest.mark.asyncio
async def test_origin_urls_exception_on_pull(self):
"""list(origin.urls) raises → caught, pull continues."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
repo = MagicMock()
repo.git_dir = "/tmp/repo/.git"
repo.working_tree_dir = "/tmp/repo"
repo.active_branch.name = "main"
origin = MagicMock()
# list(origin.urls) raises
type(origin).urls = PropertyMock(side_effect=Exception("urls error"))
repo.remote.return_value = origin
ref = MagicMock()
ref.name = "origin/main"
repo.refs = [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)
repo.git.pull.assert_called_with("--no-rebase", "origin", "main")
@pytest.mark.asyncio
async def test_git_command_error_non_conflict(self):
"""GitCommandError that is not conflict → HTTPException 500."""
from src.services.git._sync import GitServiceSyncMixin
mixin = GitServiceSyncMixin()
mixin._locked = lambda x: contextlib.nullcontext()
repo = MagicMock()
repo.git_dir = "/tmp/repo/.git"
repo.working_tree_dir = "/tmp/repo"
repo.active_branch.name = "main"
origin = MagicMock()
origin.urls = ["https://git.com/org/repo.git"]
repo.remote.return_value = origin
ref = MagicMock()
ref.name = "origin/main"
repo.refs = [ref]
repo.git.pull.side_effect = GitCommandError("pull", "fatal: not a git 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.Edge