Files
ss-tools/backend/tests/api/test_git_helpers.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
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)
2026-06-15 13:55:57 +03:00

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