Files
ss-tools/backend/tests/services/git/test_git_gitea_coverage.py

226 lines
9.9 KiB
Python

# #region Test.Git.Gitea.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, gitea, coverage, branch, pr, error]
# @BRIEF Additional edge coverage for GitServiceGiteaMixin — error JSON parse, description params, non-dict existing, branch re-raise, PR detail, fallback failure.
# @RELATION BINDS_TO -> [GitServiceGiteaMixin]
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException
from src.models.git import GitProvider
from src.services.git._gitea import GitServiceGiteaMixin
class TestableGitGitea(GitServiceGiteaMixin):
"""Concrete wrapper that inherits mixin methods and provides URL stubs."""
def __init__(self):
self._http_client = AsyncMock()
def _normalize_git_server_url(self, raw_url):
normalized = (raw_url or "").strip()
if not normalized:
raise HTTPException(status_code=400, detail="Git server URL is required")
return normalized.rstrip("/")
def _parse_remote_repo_identity(self, remote_url):
from urllib.parse import urlparse
normalized = str(remote_url or "").strip()
if not normalized:
raise HTTPException(status_code=400, detail="empty")
if normalized.startswith("git@"):
path = normalized.split(":", 1)[1] if ":" in normalized else ""
else:
parsed = urlparse(normalized)
path = parsed.path or ""
path = path.strip("/")
if path.endswith(".git"):
path = path[:-4]
parts = [s for s in path.split("/") if s]
if len(parts) < 2:
raise HTTPException(status_code=400, detail="Cannot parse")
owner, repo = parts[0], parts[-1]
namespace = "/".join(parts[:-1])
return {"owner": owner, "repo": repo, "namespace": namespace, "full_name": f"{namespace}/{repo}"}
def _derive_server_url_from_remote(self, remote_url):
from urllib.parse import urlparse
normalized = str(remote_url or "").strip()
if not normalized or normalized.startswith("git@"):
return None
parsed = urlparse(normalized)
if parsed.scheme not in {"http", "https"}:
return None
if not parsed.hostname:
return None
netloc = parsed.hostname
if parsed.port:
netloc = f"{netloc}:{parsed.port}"
return f"{parsed.scheme}://{netloc}"
@pytest.fixture
def svc():
return TestableGitGitea()
class TestGiteaRequestCoverage:
"""Cover JSON parse exception branch in _gitea_request."""
@pytest.mark.asyncio
async def test_error_response_no_json(self, svc):
"""4xx response, JSON parse fails → uses raw text for detail."""
resp = MagicMock()
resp.status_code = 403
resp.text = "forbidden"
resp.json.side_effect = ValueError("not json")
svc._http_client.request.return_value = resp
with pytest.raises(HTTPException, match="forbidden"):
await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
class TestCreateGiteaRepositoryCoverage:
"""Cover description param and non-dict existing repo response."""
@pytest.mark.asyncio
async def test_create_with_description(self, svc):
"""description provided → sent in payload."""
created = {"name": "my-repo"}
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created) as mock_req:
result = await svc.create_gitea_repository(
"https://gitea.com", "pat", "my-repo",
description="Test description",
)
assert result["name"] == "my-repo"
call_payload = mock_req.call_args[1]["payload"]
assert call_payload["description"] == "Test description"
@pytest.mark.asyncio
async def test_description_default_empty(self, svc):
"""No description → not in payload."""
created = {"name": "repo"}
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created) as mock_req:
await svc.create_gitea_repository("https://gitea.com", "pat", "repo")
call_payload = mock_req.call_args[1]["payload"]
assert "description" not in call_payload
@pytest.mark.asyncio
async def test_conflict_existing_not_dict_raises(self, svc):
"""409 conflict, existing GET returns non-dict → raises original 409."""
call_log = []
async def mock_request(method, url, pat, endpoint, payload=None):
call_log.append((method, endpoint))
if method == "POST":
raise HTTPException(status_code=409, detail="conflict")
if method == "GET" and endpoint == "/user":
return {"login": "owner"}
return ["not_a_dict"]
with patch.object(svc, "_gitea_request", side_effect=mock_request):
with pytest.raises(HTTPException, match="409"):
await svc.create_gitea_repository("https://gitea.com", "pat", "existing-repo")
class TestGiteaBranchExistsCoverage:
"""Cover non-404 re-raise in _gitea_branch_exists."""
@pytest.mark.asyncio
async def test_non_404_raises(self, svc):
"""Non-404 HTTPException → re-raised."""
with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=403, detail="forbidden")):
with pytest.raises(HTTPException, match="forbidden"):
await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "dev")
class TestBuildGiteaPR404Detail:
"""_build_gitea_pr_404_detail — all branch-combination paths."""
@pytest.mark.asyncio
async def test_source_branch_missing(self, svc):
"""Source branch not found → specific detail."""
with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock) as mock_exists:
async def exists_side(server_url, pat, owner, repo, branch):
return branch == "main"
mock_exists.side_effect = exists_side
result = await svc._build_gitea_pr_404_detail(
"https://gitea.com", "pat", "owner", "repo", "feature", "main",
)
assert "source branch" in (result or "")
@pytest.mark.asyncio
async def test_target_branch_missing(self, svc):
"""Target branch not found → specific detail."""
with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock) as mock_exists:
async def exists_side(server_url, pat, owner, repo, branch):
return branch == "feature"
mock_exists.side_effect = exists_side
result = await svc._build_gitea_pr_404_detail(
"https://gitea.com", "pat", "owner", "repo", "feature", "main",
)
assert "target branch" in (result or "")
@pytest.mark.asyncio
async def test_both_branches_exist(self, svc):
"""Both branches exist → returns None."""
with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock, return_value=True):
result = await svc._build_gitea_pr_404_detail(
"https://gitea.com", "pat", "owner", "repo", "feature", "main",
)
assert result is None
class TestCreateGiteaPRCoverage:
"""Cover fallback retry still 404 path."""
@pytest.mark.asyncio
async def test_fallback_also_404_with_detail(self, svc):
"""Fallback URL also returns 404, branch detail found → HTTP 400."""
call_count = [0]
async def mock_request(method, url, pat, endpoint, payload=None):
call_count[0] += 1
if method == "POST":
raise HTTPException(status_code=404, detail="not found")
raise HTTPException(status_code=999, detail="unexpected")
with patch.object(svc, "_gitea_request", side_effect=mock_request), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value="source branch 'feature' not found"):
with pytest.raises(HTTPException) as exc_info:
await svc.create_gitea_pull_request(
"https://gitea.com/owner", "pat",
"https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
assert exc_info.value.status_code == 400
assert "source" in exc_info.value.detail
@pytest.mark.asyncio
async def test_fallback_also_404_no_detail(self, svc):
"""Fallback also 404, branch detail None → raises original."""
async def mock_request(method, url, pat, endpoint, payload=None):
raise HTTPException(status_code=404, detail="not found")
with patch.object(svc, "_gitea_request", side_effect=mock_request), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value=None):
with pytest.raises(HTTPException, match="not found"):
await svc.create_gitea_pull_request(
"https://gitea.com/owner", "pat",
"https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
@pytest.mark.asyncio
async def test_no_fallback_no_retry(self, svc):
"""No fallback URL → no retry on 404."""
async def mock_request(method, url, pat, endpoint, payload=None):
raise HTTPException(status_code=404, detail="not found")
with patch.object(svc, "_gitea_request", side_effect=mock_request), \
patch.object(svc, "_derive_server_url_from_remote", return_value=None), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value=None):
with pytest.raises(HTTPException, match="not found"):
await svc.create_gitea_pull_request(
"https://gitea.com/owner", "pat",
"https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
# #endregion Test.Git.Gitea.Coverage