# #region Test.Api.GitRepoRoutes [C:3] [TYPE Module] [SEMANTICS test,git,repo,init,binding,branches,checkout] # @BRIEF Tests for core Git repository API routes — init, binding, branches, checkout, delete. # @RELATION BINDS_TO -> [GitRepoRoutes] 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") os.environ.setdefault("DEV_MODE", "true") 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) @pytest.fixture def mock_git_config(): config = MagicMock() config.id = "cfg-1" config.url = "https://gitea.example.com" config.pat = "pat-123" config.provider = "GITEA" config.default_branch = "main" return config def _make_client(overrides: dict | None = None) -> 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 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() app.dependency_overrides[get_current_user] = lambda: mock_user if overrides: for dep, fn in overrides.items(): app.dependency_overrides[dep] = fn return TestClient(app) # ── init_repository ── class TestInitRepository: """POST /repositories/{dashboard_ref}/init""" INIT_PAYLOAD = {"config_id": "cfg-1", "remote_url": "https://example.com/org/repo.git"} def test_success_new_repo(self, mock_git_config): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_git_config, # GitServerConfig lookup None, # GitRepository lookup (not found → create new) ] mock_gs = MagicMock() mock_gs.init_repo = AsyncMock() mock_gs._get_repo_path = AsyncMock(return_value="/tmp/repo") from src.core.database import get_db with ( patch("src.api.routes.git._repo_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), patch("src.api.routes.git._resolve_repo_key_from_ref", AsyncMock(return_value="my-dash")), ): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/repositories/42/init", json=self.INIT_PAYLOAD) assert resp.status_code == 200 data = resp.json() assert data["status"] == "success" # Verify GitRepository was created mock_db.add.assert_called_once() mock_db.commit.assert_called_once() def test_success_existing_repo(self, mock_git_config): existing_repo = MagicMock() existing_repo.dashboard_id = 42 mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_git_config, existing_repo, ] mock_gs = MagicMock() mock_gs.init_repo = AsyncMock() mock_gs._get_repo_path = AsyncMock(return_value="/tmp/repo") from src.core.database import get_db with ( patch("src.api.routes.git._repo_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), patch("src.api.routes.git._resolve_repo_key_from_ref", AsyncMock(return_value="my-dash")), ): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/repositories/42/init", json=self.INIT_PAYLOAD) assert resp.status_code == 200 # Should update existing, not add new mock_db.add.assert_not_called() mock_db.commit.assert_called_once() def test_config_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db with ( patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/repositories/42/init", json=self.INIT_PAYLOAD) assert resp.status_code == 404 def test_init_error_rollback(self, mock_git_config): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_git_config, None, ] mock_gs = MagicMock() mock_gs.init_repo = AsyncMock(side_effect=RuntimeError("init failed")) from src.core.database import get_db with ( patch("src.api.routes.git._repo_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), patch("src.api.routes.git._resolve_repo_key_from_ref", AsyncMock(return_value="my-dash")), ): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/repositories/42/init", json=self.INIT_PAYLOAD) assert resp.status_code == 500 mock_db.rollback.assert_called_once() # ── get_repository_binding ── class TestGetRepositoryBinding: """GET /repositories/{dashboard_ref}""" def test_success(self, mock_git_config): mock_repo = MagicMock() mock_repo.dashboard_id = 42 mock_repo.config_id = "cfg-1" mock_repo.remote_url = "https://example.com/repo.git" mock_repo.local_path = "/tmp/repo" mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = [ mock_repo, mock_git_config, ] from src.core.database import get_db with ( patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client({get_db: lambda: mock_db}) resp = client.get("/repositories/42") assert resp.status_code == 200 data = resp.json() assert data["dashboard_id"] == 42 assert data["provider"] == "GITEA" assert data["remote_url"] == "https://example.com/repo.git" def test_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.core.database import get_db with ( patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client({get_db: lambda: mock_db}) resp = client.get("/repositories/42") assert resp.status_code == 404 def test_unexpected_error(self, mock_git_config): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = RuntimeError("db error") from src.core.database import get_db with ( patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), ): client = _make_client({get_db: lambda: mock_db}) resp = client.get("/repositories/42") assert resp.status_code == 500 # ── delete_repository ── class TestDeleteRepository: """DELETE /repositories/{dashboard_ref}""" def test_success(self): mock_gs = MagicMock() mock_gs.delete_repo = AsyncMock() with ( patch("src.api.routes.git._repo_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.delete("/repositories/42") assert resp.status_code == 200 assert resp.json()["status"] == "success" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.delete_repo = AsyncMock(side_effect=RuntimeError("boom")) with ( patch("src.api.routes.git._repo_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.delete("/repositories/42") assert resp.status_code == 500 # ── get_branches ── class TestGetBranches: """GET /repositories/{dashboard_ref}/branches""" def test_success(self): mock_gs = MagicMock() mock_branch = MagicMock() mock_branch.name = "dev" mock_branch.commit_hash = "abc" mock_branch.is_remote = False from datetime import datetime mock_branch.last_updated = datetime.now() mock_gs.list_branches = AsyncMock(return_value=[mock_branch]) with ( patch("src.api.routes.git._repo_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/branches") assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["name"] == "dev" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.list_branches = AsyncMock(return_value=RuntimeError("boom")) with ( patch("src.api.routes.git._repo_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/branches") assert resp.status_code == 500 # ── create_branch ── class TestCreateBranch: """POST /repositories/{dashboard_ref}/branches""" BRANCH_PAYLOAD = {"name": "feature-x", "from_branch": "dev"} def test_success(self): mock_gs = MagicMock() with ( patch("src.api.routes.git._repo_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), patch("src.api.routes.git._repo_routes._apply_git_identity_from_profile", AsyncMock()), ): client = _make_client() resp = client.post("/repositories/42/branches", json=self.BRANCH_PAYLOAD) assert resp.status_code == 200 assert resp.json()["status"] == "success" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.create_branch = AsyncMock(return_value=RuntimeError("boom")) with ( patch("src.api.routes.git._repo_routes.get_git_service", return_value=mock_gs), patch("src.api.routes.git._resolve_dashboard_id_from_ref", AsyncMock(return_value=42)), patch("src.api.routes.git._repo_routes._apply_git_identity_from_profile", AsyncMock()), ): client = _make_client() resp = client.post("/repositories/42/branches", json=self.BRANCH_PAYLOAD) assert resp.status_code == 500 # ── checkout_branch ── class TestCheckoutBranch: """POST /repositories/{dashboard_ref}/checkout""" CHECKOUT_PAYLOAD = {"name": "main"} def test_success(self): mock_gs = MagicMock() with ( patch("src.api.routes.git._repo_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/checkout", json=self.CHECKOUT_PAYLOAD) assert resp.status_code == 200 assert resp.json()["status"] == "success" def test_unexpected_error(self): mock_gs = MagicMock() mock_gs.checkout_branch = AsyncMock(return_value=RuntimeError("boom")) with ( patch("src.api.routes.git._repo_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/checkout", json=self.CHECKOUT_PAYLOAD) assert resp.status_code == 500 # #endregion Test.Api.GitRepoRoutes