Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
482 lines
19 KiB
Python
482 lines
19 KiB
Python
# #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 = MagicMock()
|
|
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.api.routes.git._helpers.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.api.routes.git._helpers.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 = MagicMock()
|
|
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
|