# #region Test.Api.GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS test,git,merge,routes] # @BRIEF Tests for Git merge API routes — status, conflicts, resolve, abort, continue. # @RELATION BINDS_TO -> [GitMergeRoutes] # @TEST_EDGE: success -> 200 with expected data # @TEST_EDGE: unexpected_error -> 500 # @TEST_EDGE: dashboard_not_found -> 404 propagated from resolve_dashboard import os os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:") os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests") import sys from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) MERGE_STATUS = { "has_unfinished_merge": True, "repository_path": "/tmp/repo", "git_dir": "/tmp/repo/.git", "current_branch": "dev", "merge_head": "abc123", "merge_message_preview": "Merge branch 'feature' into dev", "conflicts_count": 2, } MERGE_CONFLICTS = [ {"file_path": "dashboard.json", "mine": "my content", "theirs": "their content"}, {"file_path": "config.yaml", "mine": "my config", "theirs": "their config"}, ] def _make_client() -> TestClient: from src.api.routes.git._router import router as parent_router from src.core.database import get_db from src.dependencies import get_current_user, has_permission from src.schemas.auth import User, RoleSchema app = FastAPI() app.include_router(parent_router) mock_user = User( id="admin-1", username="admin", email="admin@x.com", auth_source="LOCAL", created_at=__import__("datetime").datetime.now(), roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])], ) app.dependency_overrides = { get_db: lambda: MagicMock(), get_current_user: lambda: mock_user, } return TestClient(app) class TestGetMergeStatus: """GET /repositories/{dashboard_ref}/merge/status""" def test_success(self): mock_gs = MagicMock() mock_gs.get_merge_status.return_value = MERGE_STATUS with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.get("/repositories/42/merge/status") assert resp.status_code == 200 data = resp.json() assert data["has_unfinished_merge"] is True assert data["current_branch"] == "dev" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.get_merge_status.side_effect = RuntimeError("boom") with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.get("/repositories/42/merge/status") assert resp.status_code == 500 assert "boom" in resp.text def test_dashboard_not_found(self): with ( patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(side_effect=HTTPException(status_code=404, detail="Not found"))), ): client = _make_client() resp = client.get("/repositories/bad-ref/merge/status") assert resp.status_code == 404 class TestGetMergeConflicts: """GET /repositories/{dashboard_ref}/merge/conflicts""" def test_success(self): mock_gs = MagicMock() mock_gs.get_merge_conflicts.return_value = MERGE_CONFLICTS with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.get("/repositories/42/merge/conflicts") assert resp.status_code == 200 data = resp.json() assert len(data) == 2 assert data[0]["file_path"] == "dashboard.json" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.get_merge_conflicts.side_effect = RuntimeError("boom") with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.get("/repositories/42/merge/conflicts") assert resp.status_code == 500 class TestResolveMergeConflicts: """POST /repositories/{dashboard_ref}/merge/resolve""" RESOLVE_PAYLOAD = { "resolutions": [ {"file_path": "dashboard.json", "resolution": "mine"}, ] } def test_success(self): mock_gs = MagicMock() mock_gs.resolve_merge_conflicts.return_value = ["dashboard.json"] with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/resolve", json=self.RESOLVE_PAYLOAD) assert resp.status_code == 200 data = resp.json() assert data["status"] == "success" assert data["resolved_files"] == ["dashboard.json"] def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.resolve_merge_conflicts.side_effect = RuntimeError("boom") with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/resolve", json=self.RESOLVE_PAYLOAD) assert resp.status_code == 500 class TestAbortMerge: """POST /repositories/{dashboard_ref}/merge/abort""" def test_success(self): mock_gs = MagicMock() mock_gs.abort_merge.return_value = {"status": "aborted"} with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/abort") assert resp.status_code == 200 def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.abort_merge.side_effect = RuntimeError("boom") with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/abort") assert resp.status_code == 500 class TestContinueMerge: """POST /repositories/{dashboard_ref}/merge/continue""" def test_success(self): mock_gs = MagicMock() mock_gs.continue_merge.return_value = {"status": "merged"} with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/continue", json={"message": "Merge complete"}) assert resp.status_code == 200 def test_success_no_message(self): mock_gs = MagicMock() mock_gs.continue_merge.return_value = {"status": "merged"} with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/continue", json={}) assert resp.status_code == 200 def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.continue_merge.side_effect = RuntimeError("boom") with ( patch("src.api.routes.git._merge_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client() resp = client.post("/repositories/42/merge/continue", json={"message": "x"}) assert resp.status_code == 500 # #endregion Test.Api.GitMergeRoutes