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)
469 lines
23 KiB
Python
469 lines
23 KiB
Python
# #region Test.Git.Base [C:4] [TYPE Module] [SEMANTICS test,git,base,init,repo,lock,identity]
|
|
# @BRIEF Tests for GitServiceBase — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, HTTP client.
|
|
# @RELATION BINDS_TO -> [GitServiceBase]
|
|
|
|
import contextlib
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
|
|
|
|
from fastapi import HTTPException
|
|
from git.exc import InvalidGitRepositoryError, NoSuchPathError
|
|
from git import Repo
|
|
|
|
from src.services.git._base import GitServiceBase
|
|
|
|
|
|
class TestableGitServiceBase:
|
|
"""Concrete test wrapper - passes through to GitServiceBase static methods."""
|
|
pass
|
|
|
|
|
|
class TestGitServiceBase:
|
|
"""Test GitServiceBase using the actual class with mocks."""
|
|
|
|
# #region test_close_idempotent [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_close_idempotent(self):
|
|
"""Multiple close calls only close once."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
svc._http_client = AsyncMock()
|
|
await svc.close()
|
|
svc._http_client.aclose.assert_called_once()
|
|
await svc.close()
|
|
svc._http_client.aclose.assert_called_once()
|
|
assert svc._closed is True
|
|
# #endregion test_close_idempotent
|
|
|
|
# #region test_closed_service_raises [C:2] [TYPE Function]
|
|
def test_closed_service_raises(self):
|
|
"""Closed service → RuntimeError on _locked."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
svc._closed = True
|
|
with pytest.raises(RuntimeError, match="closed"):
|
|
with svc._locked(1):
|
|
pass
|
|
# #endregion test_closed_service_raises
|
|
|
|
# #region test_get_lock_creates_new [C:2] [TYPE Function]
|
|
def test_get_lock_creates_new(self):
|
|
"""New dashboard_id creates new lock."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
lock = svc._get_lock(42)
|
|
assert lock is not None
|
|
assert 42 in svc._lock_registry
|
|
assert svc._get_lock(42) is lock
|
|
# #endregion test_get_lock_creates_new
|
|
|
|
# #region test_locked_acquire_release [C:2] [TYPE Function]
|
|
def test_locked_acquire_release(self):
|
|
"""Lock is acquired on enter, released on exit."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
mock_lock = MagicMock()
|
|
svc._lock_registry[1] = mock_lock
|
|
with svc._locked(1):
|
|
mock_lock.acquire.assert_called_once()
|
|
mock_lock.release.assert_called_once()
|
|
# #endregion test_locked_acquire_release
|
|
|
|
# #region test_clone_with_auth_http [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_clone_with_auth_http(self):
|
|
"""HTTP URL with PAT → cloned with auth URL, then strips."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
mock_repo = MagicMock()
|
|
with patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_run:
|
|
mock_run.return_value = mock_repo
|
|
result = await svc._clone_with_auth("https://gitea.com/org/repo.git", "/tmp/repo", "my-pat")
|
|
clone_call = mock_run.call_args
|
|
assert clone_call[1]['fn'] == Repo.clone_from
|
|
auth_url = clone_call[1]['args'][0] if 'args' in clone_call[1] else clone_call[0][2]
|
|
# Just verify it was called
|
|
assert result == mock_repo
|
|
# #endregion test_clone_with_auth_http
|
|
|
|
# #region test_clone_with_auth_no_pat [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_clone_with_auth_no_pat(self):
|
|
"""No PAT → clones without auth modifications."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
mock_repo = MagicMock()
|
|
with patch("src.services.git._base.run_blocking", new_callable=AsyncMock) as mock_run:
|
|
mock_run.return_value = mock_repo
|
|
result = await svc._clone_with_auth("https://gitea.com/org/repo.git", "/tmp/repo", "")
|
|
assert result == mock_repo
|
|
# #endregion test_clone_with_auth_no_pat
|
|
|
|
|
|
# ── _ensure_base_path_exists ──
|
|
|
|
class TestEnsureBasePathExists:
|
|
"""_ensure_base_path_exists — directory creation."""
|
|
|
|
# #region test_ensure_base_creates [C:2] [TYPE Function]
|
|
def test_ensure_base_creates(self):
|
|
"""Non-existent path → creates directory."""
|
|
with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
with patch.object(Path, "exists", return_value=False), \
|
|
patch.object(Path, "mkdir") as mock_mkdir:
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
svc.base_path = "/tmp/test"
|
|
svc._ensure_base_path_exists()
|
|
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
|
# #endregion test_ensure_base_creates
|
|
|
|
# #region test_ensure_base_not_dir_raises [C:2] [TYPE Function]
|
|
def test_ensure_base_not_dir_raises(self):
|
|
"""Exists but not dir → ValueError."""
|
|
with patch.object(Path, "exists", return_value=True), \
|
|
patch.object(Path, "is_dir", return_value=False):
|
|
with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
svc.base_path = "/tmp/test"
|
|
with pytest.raises(ValueError, match="not a directory"):
|
|
svc._ensure_base_path_exists()
|
|
# #endregion test_ensure_base_not_dir_raises
|
|
|
|
# #region test_ensure_base_permission_error [C:2] [TYPE Function]
|
|
def test_ensure_base_permission_error(self):
|
|
"""PermissionError → ValueError."""
|
|
with patch.object(Path, "exists", return_value=False), \
|
|
patch.object(Path, "mkdir", side_effect=PermissionError("denied")):
|
|
with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
svc.base_path = "/tmp/test"
|
|
with pytest.raises(ValueError, match="Cannot create"):
|
|
svc._ensure_base_path_exists()
|
|
# #endregion test_ensure_base_permission_error
|
|
|
|
|
|
# ── _normalize_repo_key ──
|
|
|
|
class TestNormalizeRepoKey:
|
|
"""_normalize_repo_key — sanitize to filesystem-safe name."""
|
|
|
|
# #region test_normalize_basic [C:2] [TYPE Function]
|
|
def test_normalize_basic(self):
|
|
"""Normal key returned unchanged."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
assert svc._normalize_repo_key("my-repo") == "my-repo"
|
|
# #endregion test_normalize_basic
|
|
|
|
# #region test_normalize_special_chars [C:2] [TYPE Function]
|
|
def test_normalize_special_chars(self):
|
|
"""Special chars replaced with hyphens."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
assert svc._normalize_repo_key("My Repo!@#$") == "my-repo"
|
|
# #endregion test_normalize_special_chars
|
|
|
|
# #region test_normalize_empty [C:2] [TYPE Function]
|
|
def test_normalize_empty(self):
|
|
"""Empty key → 'dashboard'."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
assert svc._normalize_repo_key("") == "dashboard"
|
|
assert svc._normalize_repo_key(None) == "dashboard"
|
|
# #endregion test_normalize_empty
|
|
|
|
|
|
# ── _update_repo_local_path ──
|
|
|
|
class TestUpdateRepoLocalPath:
|
|
"""_update_repo_local_path — persist local path in DB."""
|
|
|
|
# #region test_update_path_existing [C:2] [TYPE Function]
|
|
def test_update_path_existing(self):
|
|
"""Existing DB record → local_path updated."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch("src.services.git._base.SessionLocal") as mock_session_cls:
|
|
mock_session = MagicMock()
|
|
mock_session_cls.return_value = mock_session
|
|
mock_session.query.return_value.filter.return_value.first.return_value = MagicMock(local_path="")
|
|
svc._update_repo_local_path(1, "/tmp/repo")
|
|
assert mock_session.commit.called
|
|
# #endregion test_update_path_existing
|
|
|
|
# #region test_update_path_no_record [C:2] [TYPE Function]
|
|
def test_update_path_no_record(self):
|
|
"""No DB record → no error."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch("src.services.git._base.SessionLocal") as mock_session_cls:
|
|
mock_session = MagicMock()
|
|
mock_session_cls.return_value = mock_session
|
|
mock_session.query.return_value.filter.return_value.first.return_value = None
|
|
svc._update_repo_local_path(1, "/tmp/repo")
|
|
# #endregion test_update_path_no_record
|
|
|
|
# #region test_update_path_db_error [C:2] [TYPE Function]
|
|
def test_update_path_db_error(self):
|
|
"""DB error → caught, no exception."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch("src.services.git._base.SessionLocal", side_effect=Exception("db error")):
|
|
svc._update_repo_local_path(1, "/tmp/repo")
|
|
# #endregion test_update_path_db_error
|
|
|
|
|
|
# ── _migrate_repo_directory ──
|
|
|
|
class TestMigrateRepoDirectory:
|
|
"""_migrate_repo_directory — move repo to new path."""
|
|
|
|
# #region test_migrate_same_path [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_same_path(self):
|
|
"""Source == target → returns source unchanged."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
result = await svc._migrate_repo_directory(1, "/same/path", "/same/path")
|
|
assert result == "/same/path"
|
|
# #endregion test_migrate_same_path
|
|
|
|
# #region test_migrate_target_exists [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_migrate_target_exists(self):
|
|
"""Target exists → returns source."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch("os.path.exists", return_value=True):
|
|
result = await svc._migrate_repo_directory(1, "/source", "/target")
|
|
assert result == "/source"
|
|
# #endregion test_migrate_target_exists
|
|
|
|
|
|
# ── _get_repo_path ──
|
|
|
|
class TestGetRepoPath:
|
|
"""_get_repo_path — resolve local filesystem path."""
|
|
|
|
# #region test_get_repo_path_default [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_get_repo_path_default(self):
|
|
"""Default path → base_path + normalized key."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'):
|
|
with patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/base"):
|
|
svc = GitServiceBase(base_path="/tmp/base")
|
|
svc.base_path = "/tmp/base"
|
|
svc._uses_default_base_path = False
|
|
result = await svc._get_repo_path(1, "my-key")
|
|
assert result == "/tmp/base/my-key"
|
|
# #endregion test_get_repo_path_default
|
|
|
|
# #region test_get_repo_path_none_id [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_get_repo_path_none_id(self):
|
|
"""None dashboard_id → ValueError."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with pytest.raises(ValueError, match="cannot be None"):
|
|
await svc._get_repo_path(None)
|
|
# #endregion test_get_repo_path_none_id
|
|
|
|
|
|
# ── init_repo ──
|
|
|
|
class TestInitRepo:
|
|
"""init_repo — initialize or clone repository."""
|
|
|
|
# #region test_init_repo_clone [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_init_repo_clone(self):
|
|
"""New repo → cloned."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/base"):
|
|
svc = GitServiceBase(base_path="/tmp/base")
|
|
svc.base_path = "/tmp/base"
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
|
patch("os.path.exists", return_value=False), \
|
|
patch("src.services.git._base.run_blocking", new_callable=AsyncMock), \
|
|
patch.object(svc, "_clone_with_auth", new_callable=AsyncMock) as mock_clone, \
|
|
patch.object(svc, "_ensure_gitflow_branches") as mock_flow:
|
|
mock_clone.return_value = MagicMock()
|
|
result = await svc.init_repo(1, "https://remote.com/repo.git", "pat")
|
|
mock_clone.assert_called_once()
|
|
mock_flow.assert_called_once()
|
|
assert result is not None
|
|
# #endregion test_init_repo_clone
|
|
|
|
|
|
# ── get_repo ──
|
|
|
|
class TestGetRepo:
|
|
"""get_repo — open existing repository."""
|
|
|
|
# #region test_get_repo_not_found [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_get_repo_not_found(self):
|
|
"""Repository doesn't exist → HTTPException 404."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/nonexistent"), \
|
|
patch("os.path.exists", return_value=False):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.get_repo(1)
|
|
assert exc_info.value.status_code == 404
|
|
# #endregion test_get_repo_not_found
|
|
|
|
# #region test_get_repo_open_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_get_repo_open_error(self):
|
|
"""Repo path exists but cannot be opened → HTTPException 500."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
|
patch("os.path.exists", return_value=True), \
|
|
patch("src.services.git._base.run_blocking", side_effect=Exception("corrupt")):
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.get_repo(1)
|
|
assert exc_info.value.status_code == 500
|
|
# #endregion test_get_repo_open_error
|
|
|
|
|
|
# ── configure_identity ──
|
|
|
|
class TestConfigureIdentity:
|
|
"""configure_identity — set git user/email."""
|
|
|
|
# #region test_configure_identity_success [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_configure_identity_success(self):
|
|
"""Valid username/email → config_writer called."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
mock_repo = MagicMock()
|
|
mock_config_writer = MagicMock()
|
|
mock_repo.config_writer.return_value.__enter__.return_value = mock_config_writer
|
|
with patch.object(svc, "get_repo", new_callable=AsyncMock, return_value=mock_repo):
|
|
await svc.configure_identity(1, "Alice", "alice@example.com")
|
|
mock_config_writer.set_value.assert_any_call("user", "name", "Alice")
|
|
mock_config_writer.set_value.assert_any_call("user", "email", "alice@example.com")
|
|
# #endregion test_configure_identity_success
|
|
|
|
# #region test_configure_identity_empty [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_configure_identity_empty(self):
|
|
"""Empty username/email → no-op."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "get_repo", new_callable=AsyncMock) as mock_get:
|
|
await svc.configure_identity(1, "", "")
|
|
mock_get.assert_not_called()
|
|
await svc.configure_identity(1, "user", "")
|
|
mock_get.assert_not_called()
|
|
await svc.configure_identity(1, "", "email@c.com")
|
|
mock_get.assert_not_called()
|
|
# #endregion test_configure_identity_empty
|
|
|
|
# #region test_configure_identity_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_configure_identity_error(self):
|
|
"""Config writer error → HTTPException 500."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
mock_repo = MagicMock()
|
|
mock_repo.config_writer.side_effect = Exception("config error")
|
|
with patch.object(svc, "get_repo", new_callable=AsyncMock, return_value=mock_repo):
|
|
with pytest.raises(HTTPException, match="identity"):
|
|
await svc.configure_identity(1, "Alice", "alice@c.com")
|
|
# #endregion test_configure_identity_error
|
|
|
|
|
|
# ── delete_repo ──
|
|
|
|
class TestDeleteRepo:
|
|
"""delete_repo — remove repository."""
|
|
|
|
# #region test_delete_repo_not_found [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_delete_repo_not_found(self):
|
|
"""No local repo and no DB record → HTTPException 404."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/nonexistent"), \
|
|
patch("os.path.exists", return_value=False), \
|
|
patch("src.services.git._base.SessionLocal") as mock_session_cls:
|
|
mock_session = MagicMock()
|
|
mock_session_cls.return_value = mock_session
|
|
mock_session.query.return_value.filter.return_value.first.return_value = None
|
|
with pytest.raises(HTTPException, match="not found"):
|
|
await svc.delete_repo(1)
|
|
# #endregion test_delete_repo_not_found
|
|
|
|
# #region test_delete_repo_success [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_delete_repo_success(self):
|
|
"""Local repo exists → deleted, DB record removed."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
|
patch("os.path.exists", return_value=True), \
|
|
patch("os.path.isdir", return_value=True), \
|
|
patch("src.services.git._base.run_blocking", new_callable=AsyncMock), \
|
|
patch("src.services.git._base.SessionLocal") as mock_session_cls:
|
|
mock_session = MagicMock()
|
|
mock_session_cls.return_value = mock_session
|
|
mock_db_repo = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_db_repo
|
|
await svc.delete_repo(1)
|
|
mock_session.delete.assert_called_with(mock_db_repo)
|
|
mock_session.commit.assert_called_once()
|
|
# #endregion test_delete_repo_success
|
|
|
|
# #region test_delete_repo_db_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_delete_repo_db_error(self):
|
|
"""DB error → rollback, HTTPException 500."""
|
|
with patch.object(GitServiceBase, '_ensure_base_path_exists'), \
|
|
patch.object(GitServiceBase, '_resolve_base_path', return_value="/tmp/test"):
|
|
svc = GitServiceBase(base_path="/tmp/test")
|
|
with patch.object(svc, "_get_repo_path", new_callable=AsyncMock, return_value="/tmp/repo"), \
|
|
patch("os.path.exists", return_value=False), \
|
|
patch("src.services.git._base.SessionLocal") as mock_session_cls:
|
|
mock_session = MagicMock()
|
|
mock_session_cls.return_value = mock_session
|
|
mock_db_repo = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_db_repo
|
|
mock_session.delete.side_effect = Exception("db error")
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.delete_repo(1)
|
|
assert exc_info.value.status_code == 500
|
|
mock_session.rollback.assert_called_once()
|
|
# #endregion test_delete_repo_db_error
|