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)
292 lines
12 KiB
Python
292 lines
12 KiB
Python
# #region Test.Git.RemoteProviders [C:3] [TYPE Module] [SEMANTICS test,git,github,gitlab,remote,provider]
|
|
# @BRIEF Tests for GitServiceGithubMixin and GitServiceGitlabMixin — repository creation and merge request creation via HTTP.
|
|
# @RELATION BINDS_TO -> [GitServiceRemoteMixin]
|
|
# @TEST_EDGE: create_github_repo_success -> repo created
|
|
# @TEST_EDGE: create_github_repo_enterprise -> uses enterprise API URL
|
|
# @TEST_EDGE: create_github_repo_api_error -> HTTPException
|
|
# @TEST_EDGE: create_github_repo_network_error -> HTTPException 503
|
|
# @TEST_EDGE: create_gitlab_repo_success -> repo created with normalized URLs
|
|
# @TEST_EDGE: create_gitlab_repo_api_error -> HTTPException
|
|
# @TEST_EDGE: create_gitlab_merge_request_success -> MR created
|
|
# @TEST_EDGE: create_gitlab_merge_request_error -> HTTPException
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from src.services.git._remote_providers import GitServiceGithubMixin, GitServiceGitlabMixin
|
|
|
|
|
|
class TestableGitGithub(GitServiceGithubMixin):
|
|
"""Concrete test class for GitHub mixin."""
|
|
def __init__(self):
|
|
self._http_client = AsyncMock()
|
|
|
|
def _normalize_git_server_url(self, raw_url):
|
|
return (raw_url or "").strip().rstrip("/")
|
|
|
|
|
|
class TestableGitGitlab(GitServiceGitlabMixin):
|
|
"""Concrete test class for GitLab mixin."""
|
|
def __init__(self):
|
|
self._http_client = AsyncMock()
|
|
|
|
def _normalize_git_server_url(self, raw_url):
|
|
return (raw_url or "").strip().rstrip("/")
|
|
|
|
def _parse_remote_repo_identity(self, remote_url):
|
|
from urllib.parse import urlparse
|
|
normalized = str(remote_url or "").strip()
|
|
path = urlparse(normalized).path.strip("/")
|
|
if path.endswith(".git"):
|
|
path = path[:-4]
|
|
parts = [s for s in path.split("/") if s]
|
|
owner, repo = parts[0], parts[-1]
|
|
namespace = "/".join(parts[:-1])
|
|
return {"owner": owner, "repo": repo, "namespace": namespace, "full_name": f"{namespace}/{repo}"}
|
|
|
|
|
|
# ── GitHub ──
|
|
|
|
class TestCreateGitHubRepository:
|
|
"""create_github_repository — GitHub repo creation."""
|
|
|
|
# #region test_github_repo_success [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_success(self):
|
|
"""GitHub repo created successfully."""
|
|
svc = TestableGitGithub()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {"name": "new-repo", "clone_url": "https://github.com/user/new-repo.git"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
result = await svc.create_github_repository(
|
|
"https://github.com", "pat", "new-repo",
|
|
private=True, description="test repo",
|
|
)
|
|
assert result["name"] == "new-repo"
|
|
svc._http_client.post.assert_called_once()
|
|
call_url = svc._http_client.post.call_args[0][0]
|
|
assert "api.github.com" in call_url
|
|
# #endregion test_github_repo_success
|
|
|
|
# #region test_github_repo_enterprise [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_enterprise(self):
|
|
"""GitHub Enterprise URL → uses enterprise API path."""
|
|
svc = TestableGitGithub()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {"name": "repo"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
await svc.create_github_repository(
|
|
"https://github.internal.com", "pat", "repo",
|
|
)
|
|
call_url = svc._http_client.post.call_args[0][0]
|
|
assert "/api/v3" in call_url
|
|
# #endregion test_github_repo_enterprise
|
|
|
|
# #region test_github_repo_api_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_api_error(self):
|
|
"""API error → HTTPException with detail."""
|
|
svc = TestableGitGithub()
|
|
resp = MagicMock()
|
|
resp.status_code = 422
|
|
resp.text = "Validation Failed"
|
|
resp.json.return_value = {"message": "Name already exists"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
with pytest.raises(HTTPException, match="already exists"):
|
|
await svc.create_github_repository("https://github.com", "pat", "existing-repo")
|
|
# #endregion test_github_repo_api_error
|
|
|
|
# #region test_github_repo_api_error_no_json [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_api_error_no_json(self):
|
|
"""API error without JSON → uses text."""
|
|
svc = TestableGitGithub()
|
|
resp = MagicMock()
|
|
resp.status_code = 403
|
|
resp.text = "rate limit exceeded"
|
|
resp.json.side_effect = ValueError("no json")
|
|
svc._http_client.post.return_value = resp
|
|
|
|
with pytest.raises(HTTPException, match="rate limit"):
|
|
await svc.create_github_repository("https://github.com", "pat", "repo")
|
|
# #endregion test_github_repo_api_error_no_json
|
|
|
|
# #region test_github_repo_network_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_network_error(self):
|
|
"""Network error → HTTPException 503."""
|
|
svc = TestableGitGithub()
|
|
svc._http_client.post.side_effect = Exception("connection refused")
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.create_github_repository("https://github.com", "pat", "repo")
|
|
assert exc_info.value.status_code == 503
|
|
# #endregion test_github_repo_network_error
|
|
|
|
# #region test_github_repo_default_params [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_github_repo_default_params(self):
|
|
"""Default parameters passed correctly."""
|
|
svc = TestableGitGithub()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {"name": "repo"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
await svc.create_github_repository("https://github.com", "pat", "my-repo")
|
|
call_json = svc._http_client.post.call_args[1]["json"]
|
|
assert call_json["name"] == "my-repo"
|
|
assert call_json["private"] is True
|
|
assert call_json["auto_init"] is True
|
|
assert call_json["default_branch"] == "main"
|
|
# #endregion test_github_repo_default_params
|
|
|
|
|
|
# ── GitLab ──
|
|
|
|
class TestCreateGitLabRepository:
|
|
"""create_gitlab_repository — GitLab project creation."""
|
|
|
|
# #region test_gitlab_repo_success [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_repo_success(self):
|
|
"""GitLab project created with normalized fields."""
|
|
svc = TestableGitGitlab()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {
|
|
"id": 42, "name": "new-project",
|
|
"http_url_to_repo": "https://gitlab.com/user/new-project.git",
|
|
"web_url": "https://gitlab.com/user/new-project",
|
|
"ssh_url_to_repo": "git@gitlab.com:user/new-project.git",
|
|
"path_with_namespace": "user/new-project",
|
|
}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
result = await svc.create_gitlab_repository(
|
|
"https://gitlab.com", "pat", "new-project",
|
|
private=True, description="test",
|
|
)
|
|
assert result["name"] == "new-project"
|
|
assert result["clone_url"] is not None
|
|
assert result["html_url"] is not None
|
|
assert result["ssh_url"] is not None
|
|
assert result["full_name"] is not None
|
|
# #endregion test_gitlab_repo_success
|
|
|
|
# #region test_gitlab_repo_api_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_repo_api_error(self):
|
|
"""API error → HTTPException."""
|
|
svc = TestableGitGitlab()
|
|
resp = MagicMock()
|
|
resp.status_code = 400
|
|
resp.text = "Bad Request"
|
|
resp.json.return_value = {"message": "Name has already been taken"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
with pytest.raises(HTTPException, match="already been taken"):
|
|
await svc.create_gitlab_repository("https://gitlab.com", "pat", "existing")
|
|
# #endregion test_gitlab_repo_api_error
|
|
|
|
# #region test_gitlab_repo_network_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_repo_network_error(self):
|
|
"""Network error → HTTPException 503."""
|
|
svc = TestableGitGitlab()
|
|
svc._http_client.post.side_effect = Exception("timeout")
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.create_gitlab_repository("https://gitlab.com", "pat", "repo")
|
|
assert exc_info.value.status_code == 503
|
|
# #endregion test_gitlab_repo_network_error
|
|
|
|
# #region test_gitlab_repo_normalized_fields_fallback [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_repo_normalized_fields_fallback(self):
|
|
"""Missing clone_url/html_url → uses _to_repo fields."""
|
|
svc = TestableGitGitlab()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {"name": "project"} # No clone_url etc.
|
|
svc._http_client.post.return_value = resp
|
|
|
|
result = await svc.create_gitlab_repository("https://gitlab.com", "pat", "project")
|
|
assert result["clone_url"] is None # No fallback data
|
|
assert result["full_name"] == "project"
|
|
# #endregion test_gitlab_repo_normalized_fields_fallback
|
|
|
|
|
|
class TestCreateGitLabMergeRequest:
|
|
"""create_gitlab_merge_request — GitLab MR creation."""
|
|
|
|
# #region test_gitlab_mr_success [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_mr_success(self):
|
|
"""MR created successfully."""
|
|
svc = TestableGitGitlab()
|
|
resp = MagicMock()
|
|
resp.status_code = 201
|
|
resp.json.return_value = {
|
|
"iid": 7, "web_url": "https://gitlab.com/org/repo/-/merge_requests/7",
|
|
"state": "opened",
|
|
}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
result = await svc.create_gitlab_merge_request(
|
|
"https://gitlab.com", "pat",
|
|
"https://gitlab.com/org/repo.git",
|
|
"feature", "main", "Add feature", description="desc",
|
|
)
|
|
assert result["id"] == 7
|
|
assert result["status"] == "opened"
|
|
assert "merge_requests" in result["url"]
|
|
# #endregion test_gitlab_mr_success
|
|
|
|
# #region test_gitlab_mr_api_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_mr_api_error(self):
|
|
"""API error → HTTPException."""
|
|
svc = TestableGitGitlab()
|
|
resp = MagicMock()
|
|
resp.status_code = 400
|
|
resp.text = "Bad Request"
|
|
resp.json.return_value = {"message": "Source branch not found"}
|
|
svc._http_client.post.return_value = resp
|
|
|
|
with pytest.raises(HTTPException, match="not found"):
|
|
await svc.create_gitlab_merge_request(
|
|
"https://gitlab.com", "pat",
|
|
"https://gitlab.com/org/repo.git",
|
|
"bad-branch", "main", "Title",
|
|
)
|
|
# #endregion test_gitlab_mr_api_error
|
|
|
|
# #region test_gitlab_mr_network_error [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_gitlab_mr_network_error(self):
|
|
"""Network error → HTTPException 503."""
|
|
svc = TestableGitGitlab()
|
|
svc._http_client.post.side_effect = Exception("timeout")
|
|
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
await svc.create_gitlab_merge_request(
|
|
"https://gitlab.com", "pat",
|
|
"https://gitlab.com/org/repo.git",
|
|
"feature", "main", "Title",
|
|
)
|
|
assert exc_info.value.status_code == 503
|
|
# #endregion test_gitlab_mr_network_error
|