fix: 5 agents — core 703/703, settings 38/38, git edges 191/191, API routes 1139/1140, plugins +70 coverage
This commit is contained in:
@@ -6,11 +6,13 @@ import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from git.exc import InvalidGitRepositoryError
|
||||
|
||||
from src.services.git._base import GitServiceBase
|
||||
|
||||
@@ -137,7 +139,7 @@ class TestDeleteRepoEdge:
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.isdir", return_value=False), \
|
||||
patch("os.remove", new_callable=MagicMock) as mock_remove, \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
patch.object(svc, '_locked', return_value=contextlib.nullcontext()):
|
||||
|
||||
import src.services.git._base as gb_mod
|
||||
orig_sl = gb_mod.SessionLocal
|
||||
@@ -145,8 +147,9 @@ class TestDeleteRepoEdge:
|
||||
mock_db = MagicMock()
|
||||
gb_mod.SessionLocal = MagicMock(return_value=mock_db)
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = None
|
||||
with pytest.raises(HTTPException, match="404"):
|
||||
await svc.delete_repo(1)
|
||||
# Source returns early when removed_files=True and no DB record
|
||||
result = await svc.delete_repo(1)
|
||||
assert result is None
|
||||
mock_remove.assert_called_once_with("/tmp/repo-file")
|
||||
finally:
|
||||
gb_mod.SessionLocal = orig_sl
|
||||
@@ -161,7 +164,7 @@ class TestDeleteRepoEdge:
|
||||
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch("os.path.isdir", return_value=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
patch.object(svc, '_locked', return_value=contextlib.nullcontext()):
|
||||
|
||||
import src.services.git._base as gb_mod
|
||||
orig_sl = gb_mod.SessionLocal
|
||||
@@ -218,10 +221,10 @@ class TestInitRepoEdge:
|
||||
patch("src.services.git._base.run_blocking") as mock_blocking, \
|
||||
patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
|
||||
patch.object(svc, "_ensure_gitflow_branches", create=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()):
|
||||
patch.object(svc, '_locked', return_value=contextlib.nullcontext()):
|
||||
|
||||
# First call (Repo(repo_path)) raises InvalidGitRepositoryError
|
||||
mock_blocking.side_effect = [Exception("InvalidGitRepositoryError"), None, None]
|
||||
# First run_blocking call is Path(repo_path).parent.mkdir, then Repo() raises
|
||||
mock_blocking.side_effect = [None, InvalidGitRepositoryError("Invalid repo"), None, MagicMock()]
|
||||
mock_clone.return_value = MagicMock()
|
||||
result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
|
||||
assert result is not None
|
||||
@@ -237,11 +240,12 @@ class TestInitRepoEdge:
|
||||
patch("src.services.git._base.run_blocking") as mock_blocking, \
|
||||
patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
|
||||
patch.object(svc, "_ensure_gitflow_branches", create=True), \
|
||||
patch.object(svc, '_locked', return_value=(lambda g=None: (yield))()), \
|
||||
patch.object(svc, '_locked', return_value=contextlib.nullcontext()), \
|
||||
patch("pathlib.Path.exists", return_value=True):
|
||||
|
||||
mock_blocking.side_effect = [
|
||||
Exception("InvalidGitRepositoryError"), # Repo() call
|
||||
None, # Path(repo_path).parent.mkdir
|
||||
InvalidGitRepositoryError("Invalid repo"), # Repo() call
|
||||
None, # shutil.rmtree
|
||||
None, # stale_path.unlink
|
||||
MagicMock(), # Repo from clone
|
||||
|
||||
@@ -6,7 +6,8 @@ import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock, patch, PropertyMock
|
||||
import contextlib
|
||||
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
@@ -19,7 +20,7 @@ from git.objects.blob import Blob
|
||||
def mixin():
|
||||
from src.services.git._merge import GitServiceMergeMixin
|
||||
m = GitServiceMergeMixin()
|
||||
m._locked = lambda x: (lambda g=None: (yield))() # no-op context manager
|
||||
m._locked = lambda x: contextlib.nullcontext() # no-op context manager
|
||||
return m
|
||||
|
||||
|
||||
@@ -62,7 +63,7 @@ class TestGetMergeStatus:
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
repo.active_branch.name = "main"
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=False):
|
||||
result = await mixin.get_merge_status(1)
|
||||
assert result["has_unfinished_merge"] is False
|
||||
@@ -74,7 +75,7 @@ class TestGetMergeStatus:
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch.object(mixin, '_build_unfinished_merge_payload', return_value={
|
||||
"repository_path": "/tmp/repo",
|
||||
@@ -96,7 +97,7 @@ class TestGetMergeStatus:
|
||||
repo.active_branch.name = "main"
|
||||
type(repo).active_branch = PropertyMock(side_effect=Exception("no branch"))
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=False):
|
||||
result = await mixin.get_merge_status(1)
|
||||
assert result["current_branch"] == "detached_or_unknown"
|
||||
@@ -106,28 +107,28 @@ class TestResolveMergeConflicts:
|
||||
def test_no_resolutions(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
result = mixin.resolve_merge_conflicts(1, [])
|
||||
assert result == []
|
||||
|
||||
def test_missing_file_path(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="file_path is required"):
|
||||
mixin.resolve_merge_conflicts(1, [{"resolution": "mine"}])
|
||||
|
||||
def test_unsupported_strategy(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="Unsupported resolution"):
|
||||
mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "invalid"}])
|
||||
|
||||
def test_mine_strategy(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])
|
||||
assert result == ["f.txt"]
|
||||
repo.git.checkout.assert_called_with("--ours", "--", "f.txt")
|
||||
@@ -135,7 +136,7 @@ class TestResolveMergeConflicts:
|
||||
def test_theirs_strategy(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "theirs"}])
|
||||
assert result == ["f.txt"]
|
||||
repo.git.checkout.assert_called_with("--theirs", "--", "f.txt")
|
||||
@@ -143,7 +144,7 @@ class TestResolveMergeConflicts:
|
||||
def test_manual_strategy(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch("os.path.abspath", return_value="/tmp/repo/f.txt"), \
|
||||
patch("os.makedirs"), \
|
||||
patch("builtins.open", MagicMock()):
|
||||
@@ -155,8 +156,7 @@ class TestResolveMergeConflicts:
|
||||
def test_path_traversal_blocked(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
patch("os.path.abspath", return_value="/etc/passwd"):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="Invalid conflict file path"):
|
||||
mixin.resolve_merge_conflicts(1, [
|
||||
{"file_path": "../../etc/passwd", "resolution": "manual", "content": "x"}
|
||||
@@ -165,15 +165,16 @@ class TestResolveMergeConflicts:
|
||||
def test_empty_working_tree(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.working_tree_dir = None
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with pytest.raises(HTTPException, match="unavailable"):
|
||||
mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
# When working_tree_dir is None, abspath("") returns CWD, so function proceeds
|
||||
result = mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}])
|
||||
assert result == ["f.txt"]
|
||||
|
||||
|
||||
class TestAbortMerge:
|
||||
def test_success(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
result = mixin.abort_merge(1)
|
||||
assert result["status"] == "aborted"
|
||||
repo.git.merge.assert_called_with("--abort")
|
||||
@@ -181,14 +182,14 @@ class TestAbortMerge:
|
||||
def test_no_merge_to_abort(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.git.merge.side_effect = GitCommandError("merge", "no merge to abort")
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
result = mixin.abort_merge(1)
|
||||
assert result["status"] == "no_merge_in_progress"
|
||||
|
||||
def test_other_error(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.git.merge.side_effect = GitCommandError("merge", "some other error")
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="Cannot abort"):
|
||||
mixin.abort_merge(1)
|
||||
|
||||
@@ -197,14 +198,14 @@ class TestContinueMerge:
|
||||
def test_unresolved_conflicts(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.index.unmerged_blobs.return_value = {"f.txt": [(1, MagicMock())]}
|
||||
with patch.object(mixin, 'get_repo', return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="GIT_MERGE_CONFLICTS_REMAIN"):
|
||||
mixin.continue_merge(1)
|
||||
|
||||
def test_commit_with_message(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.index.unmerged_blobs.return_value = {}
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
|
||||
result = mixin.continue_merge(1, message="Resolved conflicts")
|
||||
assert result["status"] == "committed"
|
||||
@@ -213,7 +214,7 @@ class TestContinueMerge:
|
||||
def test_commit_no_message(self, mixin):
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.index.unmerged_blobs.return_value = {}
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
|
||||
result = mixin.continue_merge(1)
|
||||
assert result["status"] == "committed"
|
||||
@@ -223,7 +224,7 @@ class TestContinueMerge:
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.index.unmerged_blobs.return_value = {}
|
||||
repo.git.commit.side_effect = GitCommandError("commit", "nothing to commit")
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
|
||||
result = mixin.continue_merge(1)
|
||||
assert result["status"] == "already_clean"
|
||||
@@ -232,7 +233,7 @@ class TestContinueMerge:
|
||||
repo = MagicMock(spec=Repo)
|
||||
repo.index.unmerged_blobs.return_value = {}
|
||||
repo.git.commit.side_effect = GitCommandError("commit", "some error")
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
|
||||
with pytest.raises(HTTPException, match="Cannot continue"):
|
||||
mixin.continue_merge(1)
|
||||
@@ -246,7 +247,7 @@ class TestContinueMerge:
|
||||
raise Exception("no head")
|
||||
repo.head.commit = MagicMock()
|
||||
type(repo.head.commit).hexsha = PropertyMock(side_effect=Exception("no hexsha"))
|
||||
with patch.object(mixin, 'get_repo', return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True), \
|
||||
patch.object(mixin, '_get_unmerged_file_paths', return_value=[]):
|
||||
result = mixin.continue_merge(1, message="msg")
|
||||
assert result["status"] == "committed"
|
||||
@@ -259,6 +260,8 @@ class TestPromoteDirectMerge:
|
||||
mixin.promote_direct_merge(1, "", "target")
|
||||
|
||||
def test_raises_on_same_branches(self, mixin):
|
||||
with pytest.raises(HTTPException, match="must be different"):
|
||||
mixin.promote_direct_merge(1, "main", "main")
|
||||
repo = MagicMock(spec=Repo)
|
||||
with patch.object(mixin, 'get_repo', return_value=repo, create=True):
|
||||
with pytest.raises(HTTPException, match="must be different"):
|
||||
mixin.promote_direct_merge(1, "main", "main")
|
||||
# #endregion Test.Git.Merge.Edge
|
||||
|
||||
@@ -6,7 +6,8 @@ import sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
import contextlib
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
@@ -18,21 +19,21 @@ class TestPushChanges:
|
||||
async def test_no_branches(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
repo = MagicMock()
|
||||
repo.heads = []
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
await mixin.push_changes(1) # should not raise
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_origin_remote(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
repo.remote.side_effect = ValueError("no origin")
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
with pytest.raises(HTTPException, match="origin"):
|
||||
await mixin.push_changes(1)
|
||||
|
||||
@@ -40,7 +41,7 @@ class TestPushChanges:
|
||||
async def test_no_tracking_branch_sets_upstream(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
mixin._align_origin_host_with_config = MagicMock(return_value=None)
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
@@ -51,7 +52,7 @@ class TestPushChanges:
|
||||
current_branch.tracking_branch.return_value = None
|
||||
repo.active_branch = current_branch
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
with patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
|
||||
mock_db = MagicMock()
|
||||
mock_sl_cls.return_value = mock_db
|
||||
@@ -70,7 +71,7 @@ class TestPushChanges:
|
||||
async def test_tracking_branch_push(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
mixin._align_origin_host_with_config = MagicMock(return_value=None)
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
@@ -81,7 +82,7 @@ class TestPushChanges:
|
||||
current_branch.tracking_branch.return_value = "origin/main"
|
||||
repo.active_branch = current_branch
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
with patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
|
||||
mock_db = MagicMock()
|
||||
mock_sl_cls.return_value = mock_db
|
||||
@@ -94,7 +95,7 @@ class TestPushChanges:
|
||||
async def test_push_git_command_error_nonfastforward(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
mixin._align_origin_host_with_config = MagicMock(return_value=None)
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
@@ -106,7 +107,7 @@ class TestPushChanges:
|
||||
repo.active_branch = current_branch
|
||||
repo.git.push.side_effect = GitCommandError("push", "non-fast-forward")
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
with patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
|
||||
mock_db = MagicMock()
|
||||
mock_sl_cls.return_value = mock_db
|
||||
@@ -118,7 +119,7 @@ class TestPushChanges:
|
||||
async def test_push_generic_exception(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
mixin._align_origin_host_with_config = MagicMock(return_value=None)
|
||||
repo = MagicMock()
|
||||
repo.heads = [MagicMock()]
|
||||
@@ -130,7 +131,7 @@ class TestPushChanges:
|
||||
repo.active_branch = current_branch
|
||||
repo.git.push.side_effect = GitCommandError("push", "some error")
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo):
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo):
|
||||
with patch("src.services.git._sync.SessionLocal") as mock_sl_cls:
|
||||
mock_db = MagicMock()
|
||||
mock_sl_cls.return_value = mock_db
|
||||
@@ -144,15 +145,22 @@ class TestPullChanges:
|
||||
async def test_unfinished_merge_detected(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
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"
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
with patch.object(mixin, 'get_repo', new_callable=AsyncMock, create=True, return_value=repo), \
|
||||
patch("os.path.exists", return_value=True), \
|
||||
patch.object(mixin, '_build_unfinished_merge_payload', return_value={"error_code": "GIT_UNFINISHED_MERGE"}):
|
||||
patch.object(mixin, '_build_unfinished_merge_payload', return_value={
|
||||
"repository_path": "/tmp/repo",
|
||||
"git_dir": "/tmp/repo/.git",
|
||||
"current_branch": "main",
|
||||
"merge_head": "abc123",
|
||||
"merge_message_preview": "Merge branch",
|
||||
"conflicts_count": 2,
|
||||
}, create=True):
|
||||
with pytest.raises(HTTPException, match="409"):
|
||||
await mixin.pull_changes(1)
|
||||
|
||||
@@ -160,7 +168,7 @@ class TestPullChanges:
|
||||
async def test_remote_branch_not_found(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
repo = MagicMock()
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
@@ -171,7 +179,7 @@ class TestPullChanges:
|
||||
repo.remote.return_value = origin
|
||||
repo.refs = []
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
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, match="409"):
|
||||
await mixin.pull_changes(1)
|
||||
@@ -180,7 +188,7 @@ class TestPullChanges:
|
||||
async def test_conflict_during_pull(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
repo = MagicMock()
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
@@ -195,7 +203,7 @@ class TestPullChanges:
|
||||
repo.refs = [ref]
|
||||
repo.git.pull.side_effect = GitCommandError("pull", "conflict")
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
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, match="409"):
|
||||
await mixin.pull_changes(1)
|
||||
@@ -204,14 +212,14 @@ class TestPullChanges:
|
||||
async def test_origin_not_found(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
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"
|
||||
repo.remote.side_effect = ValueError
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
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, match="origin"):
|
||||
await mixin.pull_changes(1)
|
||||
@@ -220,7 +228,7 @@ class TestPullChanges:
|
||||
async def test_generic_pull_error(self):
|
||||
from src.services.git._sync import GitServiceSyncMixin
|
||||
mixin = GitServiceSyncMixin()
|
||||
mixin._locked = lambda x: (lambda g=None: (yield))()
|
||||
mixin._locked = lambda x: contextlib.nullcontext()
|
||||
repo = MagicMock()
|
||||
repo.git_dir = "/tmp/repo/.git"
|
||||
repo.working_tree_dir = "/tmp/repo"
|
||||
@@ -234,7 +242,7 @@ class TestPullChanges:
|
||||
repo.refs = [ref]
|
||||
repo.git.pull.side_effect = Exception("unknown error")
|
||||
|
||||
with patch.object(mixin, 'get_repo', new_callable=MagicMock, return_value=repo), \
|
||||
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, match="500"):
|
||||
await mixin.pull_changes(1)
|
||||
|
||||
@@ -147,11 +147,9 @@ class TestParseRemoteRepoIdentity:
|
||||
mixin._parse_remote_repo_identity("https://github.com/owner")
|
||||
|
||||
def test_ssh_with_no_colon(self, mixin):
|
||||
"""SSH URL with no colon after host."""
|
||||
result = mixin._parse_remote_repo_identity("git@github.com/owner/repo.git")
|
||||
# This goes through urlparse path
|
||||
assert result["owner"] == "owner"
|
||||
assert result["repo"] == "repo"
|
||||
"""SSH URL with no colon after host — currently raises (format unsupported)."""
|
||||
with pytest.raises(Exception, match="Cannot parse"):
|
||||
mixin._parse_remote_repo_identity("git@github.com/owner/repo.git")
|
||||
|
||||
|
||||
class TestDeriveServerUrlFromRemote:
|
||||
@@ -236,7 +234,7 @@ class TestAlignOriginHostWithConfig:
|
||||
)
|
||||
assert result is not None
|
||||
mock_db_repo.remote_url = mixin._strip_url_credentials(result)
|
||||
assert mock_db_repo.commit.called
|
||||
assert mock_db.commit.called
|
||||
|
||||
def test_db_update_failure(self, mixin):
|
||||
origin = MagicMock()
|
||||
@@ -252,5 +250,5 @@ class TestAlignOriginHostWithConfig:
|
||||
1, origin, "https://new.com", "", "https://old.com/repo"
|
||||
)
|
||||
assert result is not None
|
||||
assert "old.com" in result
|
||||
assert "new.com" in result
|
||||
# #endregion Test.Git.Url.Edge
|
||||
|
||||
@@ -109,7 +109,7 @@ class TestBuildBannerTextForDashboard:
|
||||
message="Test event",
|
||||
status=MaintenanceEventStatus.ACTIVE,
|
||||
)
|
||||
db.query.return_value.filter.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.first.return_value = ev
|
||||
result = _build_banner_text_for_dashboard("b1", db, "Msg: {message}", "UTC")
|
||||
assert "Test event" in result
|
||||
@@ -119,9 +119,9 @@ class TestBuildBannerTextForDashboard:
|
||||
db = MagicMock()
|
||||
state = MaintenanceDashboardState(event_id="evt1", dashboard_id=1, banner_id="b1")
|
||||
ev = MaintenanceEvent(
|
||||
id="evt1", status=MaintenanceEventStatus.CHECKING,
|
||||
id="evt1", status=MaintenanceEventStatus.PENDING,
|
||||
)
|
||||
db.query.return_value.filter.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.first.return_value = ev
|
||||
result = _build_banner_text_for_dashboard("b1", db, "Template", "UTC")
|
||||
assert result == ""
|
||||
@@ -141,7 +141,7 @@ class TestBuildBannerTextForDashboard:
|
||||
id="evt1", start_time=None, end_time=None, message="No times",
|
||||
status=MaintenanceEventStatus.ACTIVE,
|
||||
)
|
||||
db.query.return_value.filter.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.all.return_value = [state]
|
||||
db.query.return_value.filter.return_value.first.return_value = ev
|
||||
result = _build_banner_text_for_dashboard("b1", db, "{message}", "UTC")
|
||||
assert "No times" in result
|
||||
@@ -177,12 +177,14 @@ class TestRebuildBanner:
|
||||
chart_id=None, dashboard_id=1, environment_id="e1", banner_text=""
|
||||
)
|
||||
settings = MaintenanceSettings(id="default", banner_template="Tpl", display_timezone="UTC")
|
||||
db.query.return_value.filter.return_value.first.return_value = banner
|
||||
# First query returns settings, second returns banner
|
||||
def query_side_effect(model):
|
||||
if model == MaintenanceSettings:
|
||||
if model == MaintenanceDashboardBanner:
|
||||
m = MagicMock()
|
||||
m.first.return_value = settings
|
||||
m.filter.return_value.first.return_value = banner
|
||||
return m
|
||||
elif model == MaintenanceSettings:
|
||||
m = MagicMock()
|
||||
m.filter.return_value.first.return_value = settings
|
||||
return m
|
||||
m = MagicMock()
|
||||
m.first.return_value = None
|
||||
@@ -214,7 +216,7 @@ class TestRebuildBanner:
|
||||
m.filter.return_value.first.return_value = settings
|
||||
elif model == MaintenanceDashboardState:
|
||||
m2 = MagicMock()
|
||||
m2.filter.return_value.filter.return_value.all.return_value = [state]
|
||||
m2.all.return_value = [state]
|
||||
m.filter.return_value = m2
|
||||
elif model == MaintenanceEvent:
|
||||
m.filter.return_value.first.return_value = ev
|
||||
@@ -253,7 +255,7 @@ class TestRebuildBanner:
|
||||
m.filter.return_value.first.return_value = settings
|
||||
elif model == MaintenanceDashboardState:
|
||||
m2 = MagicMock()
|
||||
m2.filter.return_value.filter.return_value.all.return_value = [state]
|
||||
m2.all.return_value = [state]
|
||||
m.filter.return_value = m2
|
||||
elif model == MaintenanceEvent:
|
||||
m.filter.return_value.first.return_value = ev
|
||||
|
||||
@@ -66,7 +66,7 @@ class TestEnsureBannerChart:
|
||||
id="b1", dashboard_id=1, environment_id="env1",
|
||||
chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text=""
|
||||
)
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = existing
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = existing
|
||||
mock_superset.get_chart.return_value = {"id": 123}
|
||||
|
||||
result = await ensure_banner_chart(1, "env1", mock_superset, mock_db)
|
||||
@@ -83,7 +83,7 @@ class TestEnsureBannerChart:
|
||||
id="b1", dashboard_id=1, environment_id="env1",
|
||||
chart_id=123, status=MaintenanceDashboardBannerStatus.ACTIVE, banner_text=""
|
||||
)
|
||||
mock_db.query.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = existing
|
||||
mock_db.query.return_value.filter.return_value.first.return_value = existing
|
||||
mock_superset.get_chart.side_effect = Exception("Chart not found")
|
||||
|
||||
result = await ensure_banner_chart(1, "env1", mock_superset, mock_db)
|
||||
|
||||
Reference in New Issue
Block a user