- ~60 new/extended test files across api, core, plugins, services, schemas: routes, superset clients, task_manager, lineage, git, translate, dashboard-testing, load-testing, migration, llm_analysis, scheduler, ssl - .coveragerc: enable branch coverage; exclude src/__tests__ (test files) and src/scripts (CLI/ops tools) from the denominator - bug fixes found while testing: * settings: PUT /settings/reports registered under duplicated prefix * schemas/lineage: FleetReportDTO missing run_status (route always 500) * dashboard_testing/baseline_inheritance: visual entry read wrong field * superset_client/_databases: logger extra name shadowed LogRecord attr * routes/datasets: _yaml_string_paths recursion without yield from * translate/sql_generator: restore explicit-type timestamp contract * baseline_catalog: remove unreachable dashboard_id fallback - conftest fixes: pytest_plugins to rootdir conftest (pytest 9), test filename collision, TMPDIR-safe integration fixtures
149 lines
6.1 KiB
Python
149 lines
6.1 KiB
Python
# #region Test.Git.UndoLastCommit [C:3] [TYPE Module] [SEMANTICS test,git,undo,soft-reset,commit]
|
|
# @BRIEF Tests for GitServiceBranchMixin.undo_last_commit — BI-safe soft-undo of unpublished commits.
|
|
# @RELATION BINDS_TO -> [Services.Branch.UndoLastCommit]
|
|
# @TEST_EDGE: no_commits -> HTTPException 409 "No commits to undo"
|
|
# @TEST_EDGE: already_pushed -> HTTPException 409 (ahead_count == 0)
|
|
# @TEST_EDGE: unpushed_with_upstream -> reset --soft HEAD~1 executed, returns undone hash/message
|
|
# @TEST_EDGE: no_upstream -> undo allowed (nothing was ever pushed)
|
|
# @TEST_EDGE: detached_head -> HTTPException 409 (active_branch raises TypeError)
|
|
|
|
import contextlib
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from src.services.git._branch import GitServiceBranchMixin
|
|
|
|
|
|
class TestableGitUndo(GitServiceBranchMixin):
|
|
"""Concrete test class providing _locked and get_repo stubs."""
|
|
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()
|
|
|
|
|
|
def _make_repo(*, has_head=True, tracking=None, ahead=1, branch_name="dev"):
|
|
repo = MagicMock()
|
|
if has_head:
|
|
head_commit = MagicMock()
|
|
head_commit.hexsha = "abc123def456"
|
|
head_commit.message = "feat: test version\n"
|
|
repo.head.commit = head_commit
|
|
else:
|
|
type(repo.head).commit = property(lambda self: (_ for _ in ()).throw(ValueError("no commits")))
|
|
|
|
active_branch = MagicMock()
|
|
active_branch.name = branch_name
|
|
active_branch.tracking_branch.return_value = tracking # None or MagicMock(name='origin/dev')
|
|
if tracking == "error":
|
|
# Detached HEAD: accessing repo.active_branch itself raises TypeError
|
|
type(repo).active_branch = property(lambda self: (_ for _ in ()).throw(TypeError("detached HEAD")))
|
|
else:
|
|
repo.active_branch = active_branch
|
|
|
|
repo.iter_commits.return_value = iter([MagicMock() for _ in range(ahead)])
|
|
return repo
|
|
|
|
|
|
class TestUndoLastCommit:
|
|
# #region Test.Git.UndoNoCommits [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_no_commits_raises_409(self):
|
|
"""Empty repo → 409 'No commits to undo'."""
|
|
repo = _make_repo(has_head=False)
|
|
svc = TestableGitUndo(repo)
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.undo_last_commit(1)
|
|
assert exc_info.value.status_code == 409
|
|
assert "No commits" in exc_info.value.detail
|
|
repo.git.reset.assert_not_called()
|
|
# #endregion Test.Git.UndoNoCommits
|
|
|
|
# #region Test.Git.UndoAlreadyPushed [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_already_pushed_raises_409(self):
|
|
"""Last commit already on remote (ahead == 0) → 409, no reset."""
|
|
tracking = MagicMock()
|
|
tracking.name = "origin/dev"
|
|
repo = _make_repo(tracking=tracking, ahead=0)
|
|
svc = TestableGitUndo(repo)
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.undo_last_commit(1)
|
|
assert exc_info.value.status_code == 409
|
|
assert "already published" in exc_info.value.detail
|
|
repo.git.reset.assert_not_called()
|
|
# #endregion Test.Git.UndoAlreadyPushed
|
|
|
|
# #region Test.Git.UndoUnpushedSuccess [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_unpushed_commit_succeeds(self):
|
|
"""Unpushed commit (ahead == 1) → soft reset executed, undone metadata returned."""
|
|
tracking = MagicMock()
|
|
tracking.name = "origin/dev"
|
|
repo = _make_repo(tracking=tracking, ahead=1)
|
|
svc = TestableGitUndo(repo)
|
|
result = await svc.undo_last_commit(1)
|
|
repo.git.reset.assert_called_once_with("--soft", "HEAD~1")
|
|
assert result["status"] == "success"
|
|
assert result["undone_hash"] == "abc123def456"
|
|
assert result["undone_message"] == "feat: test version"
|
|
# #endregion Test.Git.UndoUnpushedSuccess
|
|
|
|
# #region Test.Git.UndoNoUpstream [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_no_upstream_allowed(self):
|
|
"""No upstream tracking branch → nothing pushed, undo allowed."""
|
|
repo = _make_repo(tracking=None)
|
|
svc = TestableGitUndo(repo)
|
|
result = await svc.undo_last_commit(1)
|
|
repo.git.reset.assert_called_once_with("--soft", "HEAD~1")
|
|
assert result["status"] == "success"
|
|
# #endregion Test.Git.UndoNoUpstream
|
|
|
|
# #region Test.Git.UndoDetachedHead [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_detached_head_raises_409(self):
|
|
"""Detached HEAD (active_branch raises TypeError) → 409."""
|
|
repo = _make_repo(has_head=True, tracking="error")
|
|
svc = TestableGitUndo(repo)
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.undo_last_commit(1)
|
|
assert exc_info.value.status_code == 409
|
|
repo.git.reset.assert_not_called()
|
|
# #endregion Test.Git.UndoDetachedHead
|
|
|
|
# #region Test.Git.UndoTrackingBranchError [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_undo_tracking_branch_exception_allowed(self):
|
|
"""tracking_branch() raises → treated as no upstream, undo allowed."""
|
|
repo = MagicMock()
|
|
head_commit = MagicMock()
|
|
head_commit.hexsha = "abc123def456"
|
|
head_commit.message = "feat: test version\n"
|
|
repo.head.commit = head_commit
|
|
active_branch = MagicMock()
|
|
active_branch.name = "dev"
|
|
active_branch.tracking_branch.side_effect = Exception("cannot resolve upstream")
|
|
repo.active_branch = active_branch
|
|
svc = TestableGitUndo(repo)
|
|
result = await svc.undo_last_commit(1)
|
|
repo.git.reset.assert_called_once_with("--soft", "HEAD~1")
|
|
assert result["status"] == "success"
|
|
assert result["undone_hash"] == "abc123def456"
|
|
# #endregion Test.Git.UndoTrackingBranchError
|
|
|
|
# #endregion Test.Git.UndoLastCommit
|