# #region Test.Api.GitHelpers [C:3] [TYPE Module] [SEMANTICS test,git,helpers] # @BRIEF Unit tests for git helper functions. # @RELATION BINDS_TO -> [GitHelpers] # @TEST_EDGE: no_repo_path # @TEST_EDGE: config_not_found -> 404 # @TEST_EDGE: slug_not_found -> 404 # @TEST_EDGE: empty_ref -> 400 import os import sys from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest _src = str(Path(__file__).resolve().parent.parent.parent / "src") if _src not in sys.path: sys.path.insert(0, _src) # ── _build_no_repo_status_payload ── class TestBuildNoRepoStatusPayload: def test_payload_structure(self): from src.api.routes.git._helpers import _build_no_repo_status_payload payload = _build_no_repo_status_payload() assert payload["sync_status"] == "NO_REPO" assert payload["has_repo"] is False assert payload["is_dirty"] is False assert payload["untracked_files"] == [] # ── _handle_unexpected_git_route_error ── class TestHandleUnexpectedGitRouteError: def test_raises_http_500(self): from fastapi import HTTPException from src.api.routes.git._helpers import _handle_unexpected_git_route_error with pytest.raises(HTTPException) as exc: _handle_unexpected_git_route_error("test_route", RuntimeError("boom")) assert exc.value.status_code == 500 assert "test_route failed" in exc.value.detail # ── _resolve_repository_status ── class TestResolveRepositoryStatus: @pytest.mark.asyncio async def test_repo_path_exists(self): mock_service = MagicMock() mock_service._get_repo_path = AsyncMock(return_value="/tmp/some-repo") mock_service.get_status = AsyncMock(return_value={"sync_status": "OK"}) with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("os.path.exists", return_value=True): from src.api.routes.git._helpers import _resolve_repository_status result = await _resolve_repository_status(42) assert result["sync_status"] == "OK" @pytest.mark.asyncio async def test_repo_path_missing(self): mock_service = MagicMock() mock_service._get_repo_path = AsyncMock(return_value="/nonexistent") with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("os.path.exists", return_value=False): from src.api.routes.git._helpers import _resolve_repository_status result = await _resolve_repository_status(42) assert result["sync_status"] == "NO_REPO" @pytest.mark.asyncio async def test_get_status_returns_404(self): from fastapi import HTTPException mock_service = MagicMock() mock_service._get_repo_path = AsyncMock(return_value="/tmp/repo") mock_service.get_status = AsyncMock(side_effect=HTTPException(status_code=404, detail="No repo")) with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("os.path.exists", return_value=True): from src.api.routes.git._helpers import _resolve_repository_status result = await _resolve_repository_status(42) assert result["sync_status"] == "NO_REPO" @pytest.mark.asyncio async def test_get_status_raises_other_http(self): from fastapi import HTTPException mock_service = MagicMock() mock_service._get_repo_path = AsyncMock(return_value="/tmp/repo") mock_service.get_status = AsyncMock(side_effect=HTTPException(status_code=500, detail="Server error")) with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("os.path.exists", return_value=True): from src.api.routes.git._helpers import _resolve_repository_status with pytest.raises(HTTPException) as exc: await _resolve_repository_status(42) assert exc.value.status_code == 500 # ── _get_git_config_or_404 ── class TestGetGitConfigOr404: def test_config_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = MagicMock(id="cfg-1") from src.api.routes.git._helpers import _get_git_config_or_404 result = _get_git_config_or_404(mock_db, "cfg-1") assert result is not None def test_config_not_found(self): from fastapi import HTTPException mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None from src.api.routes.git._helpers import _get_git_config_or_404 with pytest.raises(HTTPException) as exc: _get_git_config_or_404(mock_db, "cfg-missing") assert exc.value.status_code == 404 # ── _find_dashboard_id_by_slug (sync) ── class TestFindDashboardIdBySlug: @pytest.mark.asyncio async def test_found(self): client = AsyncMock() client.get_dashboards_page.return_value = (1, [{"id": 42}]) from src.api.routes.git._helpers import _find_dashboard_id_by_slug result = await _find_dashboard_id_by_slug(client, "my-slug") assert result == 42 @pytest.mark.asyncio async def test_not_found(self): client = AsyncMock() client.get_dashboards_page.return_value = (0, []) from src.api.routes.git._helpers import _find_dashboard_id_by_slug result = await _find_dashboard_id_by_slug(client, "ghost") assert result is None @pytest.mark.asyncio async def test_exception_then_second_query(self): client = AsyncMock() client.get_dashboards_page.side_effect = [ Exception("Fail"), (1, [{"id": 99}]), ] from src.api.routes.git._helpers import _find_dashboard_id_by_slug result = await _find_dashboard_id_by_slug(client, "retry") assert result == 99 # ── _resolve_dashboard_id_from_ref (sync) ── class TestResolveDashboardIdFromRef: @pytest.mark.asyncio async def test_numeric_ref(self): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref result = await _resolve_dashboard_id_from_ref("123", MagicMock()) assert result == 123 @pytest.mark.asyncio async def test_slug_ref(self): mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = AsyncMock() client.get_dashboards_page.return_value = (1, [{"id": 42}]) with patch("src.api.routes.git._helpers.SupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref result = await _resolve_dashboard_id_from_ref("my-slug", mock_config, "env-1") assert result == 42 @pytest.mark.asyncio async def test_empty_ref(self): from fastapi import HTTPException from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref("", MagicMock()) assert exc.value.status_code == 400 @pytest.mark.asyncio async def test_slug_no_env_id(self): from fastapi import HTTPException from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref("slug", MagicMock()) assert exc.value.status_code == 400 assert "env_id is required" in exc.value.detail @pytest.mark.asyncio async def test_env_not_found(self): from fastapi import HTTPException mock_config = MagicMock() mock_config.get_environments.return_value = [] from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref("slug", mock_config, "env-ghost") assert exc.value.status_code == 404 @pytest.mark.asyncio async def test_slug_not_found(self): from fastapi import HTTPException mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = MagicMock() client.get_dashboards_page.return_value = (0, []) with patch("src.api.routes.git._helpers.SupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref("ghost-slug", mock_config, "env-1") assert exc.value.status_code == 404 # ── _find_dashboard_id_by_slug_async ── class TestFindDashboardIdBySlugAsync: @pytest.mark.asyncio async def test_found(self): client = AsyncMock() client.get_dashboards_page.return_value = (1, [{"id": 42}]) from src.api.routes.git._helpers import _find_dashboard_id_by_slug_async result = await _find_dashboard_id_by_slug_async(client, "slug") assert result == 42 @pytest.mark.asyncio async def test_not_found(self): client = AsyncMock() client.get_dashboards_page.return_value = (0, []) from src.api.routes.git._helpers import _find_dashboard_id_by_slug_async result = await _find_dashboard_id_by_slug_async(client, "ghost") assert result is None # ── _resolve_dashboard_id_from_ref_async ── class TestResolveDashboardIdFromRefAsync: @pytest.mark.asyncio async def test_numeric_ref(self): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref_async result = await _resolve_dashboard_id_from_ref_async("123", MagicMock()) assert result == 123 @pytest.mark.asyncio async def test_slug_ref(self): mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = AsyncMock() client.get_dashboards_page.return_value = (1, [{"id": 42}]) client.aclose = AsyncMock() with patch("src.core.async_superset_client.AsyncSupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref_async result = await _resolve_dashboard_id_from_ref_async("slug", mock_config, "env-1") assert result == 42 client.aclose.assert_awaited_once() @pytest.mark.asyncio async def test_empty_ref(self): from fastapi import HTTPException from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref_async with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref_async("", MagicMock()) assert exc.value.status_code == 400 @pytest.mark.asyncio async def test_slug_not_found_async(self): from fastapi import HTTPException mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = AsyncMock() client.get_dashboards_page.return_value = (0, []) client.aclose = AsyncMock() with patch("src.core.async_superset_client.AsyncSupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref_async with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref_async("ghost", mock_config, "env-1") assert exc.value.status_code == 404 client.aclose.assert_awaited_once() @pytest.mark.asyncio async def test_no_env_id(self): from fastapi import HTTPException from src.api.routes.git._helpers import _resolve_dashboard_id_from_ref_async with pytest.raises(HTTPException) as exc: await _resolve_dashboard_id_from_ref_async("slug", MagicMock()) assert exc.value.status_code == 400 # ── _resolve_repo_key_from_ref ── class TestResolveRepoKeyFromRef: @pytest.mark.asyncio async def test_slug_ref_returns_slug(self): from src.api.routes.git._helpers import _resolve_repo_key_from_ref result = await _resolve_repo_key_from_ref("my-dash", 42, MagicMock()) assert result == "my-dash" @pytest.mark.asyncio async def test_numeric_ref_returns_dashboard_id_fallback(self): from src.api.routes.git._helpers import _resolve_repo_key_from_ref result = await _resolve_repo_key_from_ref("123", 42, MagicMock()) assert result == "dashboard-42" @pytest.mark.asyncio async def test_numeric_ref_with_env_lookup(self): mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = AsyncMock() client.get_dashboard.return_value = {"result": {"slug": "real-slug"}} with patch("src.api.routes.git._helpers.SupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_repo_key_from_ref result = await _resolve_repo_key_from_ref("123", 42, mock_config, "env-1") assert result == "real-slug" @pytest.mark.asyncio async def test_env_lookup_fails_uses_fallback(self): mock_config = MagicMock() env = MagicMock() env.id = "env-1" mock_config.get_environments.return_value = [env] client = MagicMock() client.get_dashboard.side_effect = Exception("API error") with patch("src.api.routes.git._helpers.SupersetClient", return_value=client): from src.api.routes.git._helpers import _resolve_repo_key_from_ref result = await _resolve_repo_key_from_ref("123", 42, mock_config, "env-1") assert result == "dashboard-42" @pytest.mark.asyncio async def test_empty_ref_uses_fallback(self): from src.api.routes.git._helpers import _resolve_repo_key_from_ref result = await _resolve_repo_key_from_ref("", 42, MagicMock()) assert result == "dashboard-42" # ── _sanitize_optional_identity_value ── class TestSanitizeOptionalIdentityValue: def test_none(self): from src.api.routes.git._helpers import _sanitize_optional_identity_value assert _sanitize_optional_identity_value(None) is None def test_empty(self): from src.api.routes.git._helpers import _sanitize_optional_identity_value assert _sanitize_optional_identity_value("") is None def test_whitespace(self): from src.api.routes.git._helpers import _sanitize_optional_identity_value assert _sanitize_optional_identity_value(" ") is None def test_valid(self): from src.api.routes.git._helpers import _sanitize_optional_identity_value assert _sanitize_optional_identity_value(" alice ") == "alice" # ── _resolve_current_user_git_identity ── class TestResolveCurrentUserGitIdentity: def test_db_is_none(self): from src.api.routes.git._helpers import _resolve_current_user_git_identity assert _resolve_current_user_git_identity(None, MagicMock()) is None def test_no_user_id(self): from src.api.routes.git._helpers import _resolve_current_user_git_identity user = MagicMock() user.id = None assert _resolve_current_user_git_identity(MagicMock(), user) is None def test_preference_found(self): mock_db = MagicMock() pref = MagicMock() pref.git_username = "alice" pref.git_email = "alice@example.com" mock_db.query.return_value.filter.return_value.first.return_value = pref user = MagicMock() user.id = "user-1" from src.api.routes.git._helpers import _resolve_current_user_git_identity result = _resolve_current_user_git_identity(mock_db, user) assert result == ("alice", "alice@example.com") def test_preference_not_found(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.return_value = None user = MagicMock() user.id = "user-1" from src.api.routes.git._helpers import _resolve_current_user_git_identity assert _resolve_current_user_git_identity(mock_db, user) is None def test_preference_missing_git_fields(self): mock_db = MagicMock() pref = MagicMock() pref.git_username = None pref.git_email = None mock_db.query.return_value.filter.return_value.first.return_value = pref user = MagicMock() user.id = "user-1" from src.api.routes.git._helpers import _resolve_current_user_git_identity assert _resolve_current_user_git_identity(mock_db, user) is None def test_query_exception(self): mock_db = MagicMock() mock_db.query.return_value.filter.return_value.first.side_effect = Exception("DB error") user = MagicMock() user.id = "user-1" from src.api.routes.git._helpers import _resolve_current_user_git_identity assert _resolve_current_user_git_identity(mock_db, user) is None def test_db_without_query_attr(self): from src.api.routes.git._helpers import _resolve_current_user_git_identity assert _resolve_current_user_git_identity(object(), MagicMock()) is None # ── _apply_git_identity_from_profile ── class TestApplyGitIdentityFromProfile: @pytest.mark.asyncio async def test_identity_resolved_and_applied(self): mock_service = MagicMock() mock_service.configure_identity = AsyncMock() with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("src.api.routes.git._helpers._resolve_current_user_git_identity", return_value=("alice", "alice@example.com")): from src.api.routes.git._helpers import _apply_git_identity_from_profile await _apply_git_identity_from_profile(42, MagicMock(), MagicMock()) mock_service.configure_identity.assert_awaited_once_with(42, "alice", "alice@example.com") @pytest.mark.asyncio async def test_identity_not_resolved(self): with patch("src.api.routes.git._helpers._resolve_current_user_git_identity", return_value=None): from src.api.routes.git._helpers import _apply_git_identity_from_profile # Should not raise await _apply_git_identity_from_profile(42, MagicMock(), MagicMock()) @pytest.mark.asyncio async def test_service_has_no_configure_identity(self): mock_service = MagicMock(spec=[]) # no configure_identity attribute with patch("src.api.routes.git._helpers.get_git_service", return_value=mock_service), \ patch("src.api.routes.git._helpers._resolve_current_user_git_identity", return_value=("alice", "alice@example.com")): from src.api.routes.git._helpers import _apply_git_identity_from_profile # Should not raise await _apply_git_identity_from_profile(42, MagicMock(), MagicMock()) # #endregion Test.Api.GitHelpers