# #region Test.Api.GitConfigRoutes [C:3] [TYPE Module] [SEMANTICS test,git,config,routes] # @BRIEF Unit tests for Git config API routes — CRUD + test connection. # @RELATION BINDS_TO -> [GitConfigRoutes] # @TEST_EDGE: config_not_found -> 404 # @TEST_EDGE: connection_fail -> 400 # @TEST_EDGE: pat_masking 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) def _make_client(overrides: dict | None = None) -> TestClient: from src.api.routes.git._config_routes import 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() # Git routers have no prefix — added via app.include_router in app.py app.include_router(router, prefix="/api/git") 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=[])], ) defaults = { get_db: lambda: MagicMock(), get_current_user: lambda: mock_user, } if overrides: defaults.update(overrides) for dep, mock_fn in defaults.items(): app.dependency_overrides[dep] = mock_fn return TestClient(app) def _make_mock_config( id: str = "cfg-1", name: str = "My Git", provider: str = "GITHUB", url: str = "https://github.com", pat: str = "ghp_secret123", last_validated: str = "2024-01-01T00:00:00", ): from src.models.git import GitStatus config = MagicMock() config.id = id config.name = name config.provider = provider config.url = url config.pat = pat config.status = GitStatus.CONNECTED config.last_validated = last_validated config.default_repository = None config.default_branch = "main" return config # ── get_git_configs ── class TestGetGitConfigs: """GET /api/git/config""" def test_list_configs_success(self): """Happy path: returns masked configs.""" from src.core.database import get_db mock_db = MagicMock() mock_db.query.return_value.all.return_value = [_make_mock_config()] client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/git/config") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert data[0]["pat"] == "********" def test_list_configs_empty(self): """Empty list returns 200.""" from src.core.database import get_db mock_db = MagicMock() mock_db.query.return_value.all.return_value = [] client = _make_client({get_db: lambda: mock_db}) resp = client.get("/api/git/config") assert resp.status_code == 200 assert resp.json() == [] # ── create_git_config ── class TestCreateGitConfig: """POST /api/git/config""" def test_create_config_success(self): """Happy path: config created.""" from datetime import datetime from src.core.database import get_db from src.models.git import GitStatus mock_db = MagicMock() def _refresh_side_effect(obj): obj.id = "new-cfg-id" obj.status = GitStatus.UNKNOWN obj.last_validated = datetime.now() mock_db.refresh.side_effect = _refresh_side_effect client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/git/config", json={ "name": "New Git", "provider": "GITHUB", "url": "https://github.com/org", "pat": "ghp_newtoken", "default_branch": "main", }) assert resp.status_code == 200 # ── update_git_config ── class TestUpdateGitConfig: """PUT /api/git/config/{config_id}""" def test_update_config_success(self): """Happy path: config updated.""" from src.core.database import get_db mock_db = MagicMock() existing = _make_mock_config(id="cfg-1", pat="ghp_secret") mock_db.query.return_value.filter.return_value.first.return_value = existing client = _make_client({get_db: lambda: mock_db}) resp = client.put("/api/git/config/cfg-1", json={"name": "Updated Git"}) assert resp.status_code == 200 assert existing.name == "Updated Git" assert resp.json()["pat"] == "********" def test_update_config_not_found(self): """Non-existent config returns 404.""" from src.core.database import get_db mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None client = _make_client({get_db: lambda: mock_db}) resp = client.put("/api/git/config/cfg-999", json={"name": "Ghost"}) assert resp.status_code == 404 def test_update_config_preserves_pat(self): """When pat is ********, existing PAT preserved.""" from src.core.database import get_db mock_db = MagicMock() existing = _make_mock_config(id="cfg-1", pat="ghp_secret") mock_db.query.return_value.filter.return_value.first.return_value = existing client = _make_client({get_db: lambda: mock_db}) resp = client.put("/api/git/config/cfg-1", json={"pat": "********"}) assert resp.status_code == 200 assert existing.pat == "ghp_secret" # ── delete_git_config ── class TestDeleteGitConfig: """DELETE /api/git/config/{config_id}""" def test_delete_config_success(self): """Happy path: config deleted.""" from src.core.database import get_db mock_db = MagicMock() existing = _make_mock_config() mock_db.query.return_value.filter.return_value.first.return_value = existing client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/git/config/cfg-1") assert resp.status_code == 200 assert resp.json()["status"] == "success" mock_db.delete.assert_called_once_with(existing) mock_db.commit.assert_called_once() def test_delete_config_not_found(self): """Non-existent config returns 404.""" from src.core.database import get_db mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None client = _make_client({get_db: lambda: mock_db}) resp = client.delete("/api/git/config/cfg-999") assert resp.status_code == 404 # ── test_git_config ── class TestTestGitConfig: """POST /api/git/config/test""" def test_connection_success(self): """Success returns 200.""" from src.core.database import get_db mock_db = MagicMock() mock_service = AsyncMock() mock_service.test_connection.return_value = True with patch("src.api.routes.git._config_routes.get_git_service", return_value=mock_service): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/git/config/test", json={ "name": "Test", "provider": "GITHUB", "url": "https://github.com", "pat": "ghp_test", "default_branch": "main", }) assert resp.status_code == 200 assert resp.json()["status"] == "success" def test_connection_fail(self): """Failure returns 400.""" from src.core.database import get_db mock_db = MagicMock() mock_service = AsyncMock() mock_service.test_connection.return_value = False with patch("src.api.routes.git._config_routes.get_git_service", return_value=mock_service): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/git/config/test", json={ "name": "Test", "provider": "GITHUB", "url": "https://github.com", "pat": "ghp_fail", }) assert resp.status_code == 400 assert "Connection failed" in resp.text def test_connection_with_masked_pat_and_config_id(self): """Masked PAT resolved from existing config by config_id.""" from src.core.database import get_db mock_db = MagicMock() existing = _make_mock_config(id="cfg-1", pat="ghp_resolved") # First filter call (by id) returns existing mock_q = MagicMock() mock_f = MagicMock() mock_f.first.return_value = existing mock_q.filter.return_value = mock_f mock_db.query.return_value = mock_q mock_service = AsyncMock() mock_service.test_connection.return_value = True with patch("src.api.routes.git._config_routes.get_git_service", return_value=mock_service): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/git/config/test", json={ "name": "Test", "provider": "GITHUB", "url": "https://github.com", "pat": "********", "config_id": "cfg-1", }) assert resp.status_code == 200 mock_service.test_connection.assert_called_with("GITHUB", "https://github.com", "ghp_resolved") def test_connection_with_masked_pat_and_url_fallback(self): """Masked PAT resolved by URL match when config_id not provided.""" from src.core.database import get_db mock_db = MagicMock() existing = _make_mock_config(id="cfg-2", pat="ghp_url_resolved") # Mock: query.filter().first() returns existing config mock_q = MagicMock() mock_f = MagicMock() mock_f.first.return_value = existing mock_q.filter.return_value = mock_f mock_db.query.return_value = mock_q mock_service = AsyncMock() mock_service.test_connection.return_value = True with patch("src.api.routes.git._config_routes.get_git_service", return_value=mock_service): client = _make_client({get_db: lambda: mock_db}) resp = client.post("/api/git/config/test", json={ "name": "Test", "provider": "GITHUB", "url": "https://github.com", "pat": "********", }) assert resp.status_code == 200 mock_service.test_connection.assert_called_with("GITHUB", "https://github.com", "ghp_url_resolved") # #endregion Test.Api.GitConfigRoutes