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

687 lines
30 KiB
Python

# #region Test.Git.Gitea [C:3] [TYPE Module] [SEMANTICS test,git,gitea,api,http,pull-request]
# @BRIEF Tests for GitServiceGiteaMixin — connection testing, headers, API requests, user resolution, repo CRUD, PR creation with fallback.
# @RELATION BINDS_TO -> [GitServiceGiteaMixin]
# @TEST_EDGE: local_url -> returns True without API call
# @TEST_EDGE: invalid_protocol -> returns False
# @TEST_EDGE: empty_pat -> returns False
# @TEST_EDGE: api_error -> HTTPException with status code
# @TEST_EDGE: network_error -> HTTPException 503
# @TEST_EDGE: empty_pat_headers -> HTTPException 400
# @TEST_EDGE: create_repo_conflict -> fetches existing repo
# @TEST_EDGE: pr_404_fallback -> retries with derived URL
# @TEST_EDGE: pr_404_branch_missing -> HTTPException 400 with detail
# @TEST_EDGE: delete_missing_owner -> HTTPException 400
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.models.git import GitProvider
from src.services.git._gitea import GitServiceGiteaMixin
class TestableGitGitea(GitServiceGiteaMixin):
"""Concrete test class providing _http_client and URL mixin 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}"
# ── test_connection ──
class TestTestConnection:
"""test_connection — provider connectivity check."""
# #region test_connection_local_url [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_local_url(self):
"""Local/localhost URL → True without API call."""
svc = TestableGitGitea()
result = await svc.test_connection(GitProvider.GITEA, "http://gitea.local:3000", "pat")
assert result is True
svc._http_client.get.assert_not_called()
# #endregion test_connection_local_url
# #region test_connection_invalid_protocol [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_invalid_protocol(self):
"""Non-HTTP URL → False."""
svc = TestableGitGitea()
result = await svc.test_connection(GitProvider.GITEA, "ftp://host.com", "pat")
assert result is False
# #endregion test_connection_invalid_protocol
# #region test_connection_empty_pat [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_empty_pat(self):
"""Empty PAT → False."""
svc = TestableGitGitea()
result = await svc.test_connection(GitProvider.GITEA, "https://gitea.com", "")
assert result is False
# #endregion test_connection_empty_pat
# #region test_connection_gitea_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_gitea_success(self):
"""Gitea 200 → True."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 200
svc._http_client.get.return_value = resp
result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "valid-pat")
assert result is True
svc._http_client.get.assert_called_once()
call_url = svc._http_client.get.call_args[0][0]
assert "/api/v1/user" in call_url
# #endregion test_connection_gitea_success
# #region test_connection_github_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_github_success(self):
"""GitHub 200 → True."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 200
svc._http_client.get.return_value = resp
result = await svc.test_connection(GitProvider.GITHUB, "https://github.com", "valid-pat")
assert result is True
call_url = svc._http_client.get.call_args[0][0]
assert "api.github.com" in call_url
# #endregion test_connection_github_success
# #region test_connection_gitlab_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_gitlab_success(self):
"""GitLab 200 → True."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 200
svc._http_client.get.return_value = resp
result = await svc.test_connection(GitProvider.GITLAB, "https://gitlab.com", "valid-pat")
assert result is True
call_url = svc._http_client.get.call_args[0][0]
assert "/api/v4/user" in call_url
# #endregion test_connection_gitlab_success
# #region test_connection_failure_status [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_failure_status(self):
"""Non-200 response → False."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 401
svc._http_client.get.return_value = resp
result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "bad-pat")
assert result is False
# #endregion test_connection_failure_status
# #region test_connection_network_error [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_network_error(self):
"""Network exception → False."""
svc = TestableGitGitea()
svc._http_client.get.side_effect = Exception("connection refused")
result = await svc.test_connection(GitProvider.GITEA, "https://gitea.example.com", "pat")
assert result is False
# #endregion test_connection_network_error
# #region test_connection_unknown_provider [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_connection_unknown_provider(self):
"""Unknown provider → False."""
svc = TestableGitGitea()
result = await svc.test_connection("BITBUCKET", "https://bitbucket.org", "pat")
assert result is False
# #endregion test_connection_unknown_provider
# ── _gitea_headers ──
class TestGiteaHeaders:
"""_gitea_headers — authorization header construction."""
# #region test_headers_valid_pat [C:2] [TYPE Function]
def test_headers_valid_pat(self):
"""Valid PAT → Authorization token header."""
svc = TestableGitGitea()
headers = svc._gitea_headers("my-token")
assert headers["Authorization"] == "token my-token"
assert headers["Content-Type"] == "application/json"
# #endregion test_headers_valid_pat
# #region test_headers_empty_pat [C:2] [TYPE Function]
def test_headers_empty_pat(self):
"""Empty PAT → HTTPException 400."""
svc = TestableGitGitea()
with pytest.raises(HTTPException, match="PAT is required"):
svc._gitea_headers("")
# #endregion test_headers_empty_pat
# ── _gitea_request ──
class TestGiteaRequest:
"""_gitea_request — HTTP request wrapper with error mapping."""
# #region test_request_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_request_success(self):
"""200 response → JSON payload returned."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 200
resp.json.return_value = {"login": "admin"}
svc._http_client.request.return_value = resp
result = await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
assert result == {"login": "admin"}
# #endregion test_request_success
# #region test_request_204_no_content [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_request_204_no_content(self):
"""204 → returns None."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 204
svc._http_client.request.return_value = resp
result = await svc._gitea_request("DELETE", "https://gitea.com", "pat", "/repos/o/r")
assert result is None
# #endregion test_request_204_no_content
# #region test_request_api_error [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_request_api_error(self):
"""4xx/5xx → HTTPException with detail."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 404
resp.text = "Not Found"
resp.json.return_value = {"message": "repository not found"}
svc._http_client.request.return_value = resp
with pytest.raises(HTTPException, match="repository not found"):
await svc._gitea_request("GET", "https://gitea.com", "pat", "/repos/o/r")
# #endregion test_request_api_error
# #region test_request_network_error [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_request_network_error(self):
"""Network exception → HTTPException 503."""
svc = TestableGitGitea()
svc._http_client.request.side_effect = Exception("connection refused")
with pytest.raises(HTTPException, match="unavailable"):
await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
# #endregion test_request_network_error
# ── get_gitea_current_user ──
class TestGetGiteaCurrentUser:
"""get_gitea_current_user — resolve authenticated username."""
# #region test_current_user_login [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_current_user_login(self):
"""Returns 'login' field from API response."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"login": "admin"}):
result = await svc.get_gitea_current_user("https://gitea.com", "pat")
assert result == "admin"
# #endregion test_current_user_login
# #region test_current_user_username_fallback [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_current_user_username_fallback(self):
"""Falls back to 'username' field."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"username": "bob"}):
result = await svc.get_gitea_current_user("https://gitea.com", "pat")
assert result == "bob"
# #endregion test_current_user_username_fallback
# #region test_current_user_missing [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_current_user_missing(self):
"""No login/username → HTTPException 500."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={}):
with pytest.raises(HTTPException, match="Failed to resolve"):
await svc.get_gitea_current_user("https://gitea.com", "pat")
# #endregion test_current_user_missing
# ── list_gitea_repositories ──
class TestListGiteaRepositories:
"""list_gitea_repositories — list user repos."""
# #region test_list_repos_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_list_repos_success(self):
"""Returns list from API."""
svc = TestableGitGitea()
repos = [{"name": "repo1"}, {"name": "repo2"}]
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=repos):
result = await svc.list_gitea_repositories("https://gitea.com", "pat")
assert len(result) == 2
# #endregion test_list_repos_success
# #region test_list_repos_non_list [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_list_repos_non_list(self):
"""Non-list response → returns []."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"error": "bad"}):
result = await svc.list_gitea_repositories("https://gitea.com", "pat")
assert result == []
# #endregion test_list_repos_non_list
# ── create_gitea_repository ──
class TestCreateGiteaRepository:
"""create_gitea_repository — create or fetch existing."""
# #region test_create_repo_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_success(self):
"""200 → returns created repo dict."""
svc = TestableGitGitea()
created = {"name": "new-repo", "clone_url": "https://gitea.com/user/new-repo.git"}
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=created):
result = await svc.create_gitea_repository("https://gitea.com", "pat", "new-repo")
assert result["name"] == "new-repo"
# #endregion test_create_repo_success
# #region test_create_repo_conflict_fetches_existing [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_conflict_fetches_existing(self):
"""409 → fetches existing repo by name."""
svc = TestableGitGitea()
existing = {"name": "existing-repo", "clone_url": "https://gitea.com/user/existing-repo.git"}
call_count = 0
async def mock_request(method, url, pat, endpoint, payload=None):
nonlocal call_count
call_count += 1
if method == "POST":
raise HTTPException(status_code=409, detail="conflict")
if endpoint.startswith("/user"):
return {"login": "user"}
return existing
with patch.object(svc, "_gitea_request", side_effect=mock_request):
result = await svc.create_gitea_repository("https://gitea.com", "pat", "existing-repo")
assert result["name"] == "existing-repo"
# #endregion test_create_repo_conflict_fetches_existing
# #region test_create_repo_unexpected_response [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_unexpected_response(self):
"""Non-dict response → HTTPException 500."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value="not a dict"):
with pytest.raises(HTTPException, match="Unexpected"):
await svc.create_gitea_repository("https://gitea.com", "pat", "repo")
# #endregion test_create_repo_unexpected_response
# ── delete_gitea_repository ──
class TestDeleteGiteaRepository:
"""delete_gitea_repository — delete remote repo."""
# #region test_delete_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_delete_success(self):
"""Valid owner/repo → DELETE request made."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=None) as mock_req:
await svc.delete_gitea_repository("https://gitea.com", "pat", "owner", "repo")
mock_req.assert_called_once()
assert mock_req.call_args[0][0] == "DELETE"
# #endregion test_delete_success
# #region test_delete_missing_owner [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_delete_missing_owner(self):
"""Empty owner → HTTPException 400."""
svc = TestableGitGitea()
with pytest.raises(HTTPException, match="required"):
await svc.delete_gitea_repository("https://gitea.com", "pat", "", "repo")
# #endregion test_delete_missing_owner
# ── _gitea_branch_exists ──
class TestGiteaBranchExists:
"""_gitea_branch_exists — check branch existence."""
# #region test_branch_exists_true [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_branch_exists_true(self):
"""200 → True."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value={"name": "dev"}):
result = await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "dev")
assert result is True
# #endregion test_branch_exists_true
# #region test_branch_exists_404 [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_branch_exists_404(self):
"""404 → False."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=404)):
result = await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "missing")
assert result is False
# #endregion test_branch_exists_404
# #region test_branch_exists_empty_params [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_branch_exists_empty_params(self):
"""Empty owner/repo/branch → False."""
svc = TestableGitGitea()
assert await svc._gitea_branch_exists("https://gitea.com", "pat", "", "repo", "dev") is False
assert await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "", "dev") is False
assert await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "") is False
# #endregion test_branch_exists_empty_params
# ── create_gitea_pull_request ──
class TestCreateGiteaPullRequest:
"""create_gitea_pull_request — PR creation with fallback."""
# #region test_pr_success [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_success(self):
"""Successful PR → normalized metadata."""
svc = TestableGitGitea()
pr_data = {"number": 42, "html_url": "https://gitea.com/o/r/pulls/42", "state": "open"}
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value=pr_data):
result = await svc.create_gitea_pull_request(
"https://gitea.com", "pat", "https://gitea.com/owner/repo.git",
"feature", "main", "Add feature",
)
assert result["id"] == 42
assert result["status"] == "open"
assert "pulls/42" in result["url"]
# #endregion test_pr_success
# #region test_pr_404_non_retryable [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_404_non_retryable(self):
"""404 with no fallback URL → raises with branch detail."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=404, detail="not found")), \
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", "pat", "https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
# #endregion test_pr_404_non_retryable
# #region test_pr_unexpected_response [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_unexpected_response(self):
"""Non-dict response → HTTPException 500."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", new_callable=AsyncMock, return_value="not a dict"):
with pytest.raises(HTTPException, match="Unexpected"):
await svc.create_gitea_pull_request(
"https://gitea.com", "pat", "https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
# #endregion test_pr_unexpected_response
# #region test_pr_404_with_branch_detail [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_404_with_branch_detail(self):
"""404 with branch missing → HTTPException 400 with detail."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=404, detail="not found")), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value="Gitea branch not found: source branch 'feature' in owner/repo"):
with pytest.raises(HTTPException) as exc_info:
await svc.create_gitea_pull_request(
"https://gitea.com", "pat", "https://gitea.com/owner/repo.git",
"feature", "main", "PR title",
)
assert exc_info.value.status_code == 400
# #endregion test_pr_404_with_branch_detail
# #region test_pr_404_retry_with_fallback [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_404_retry_with_fallback(self):
"""404 with different fallback URL → retries with fallback."""
svc = TestableGitGitea()
call_count = 0
async def mock_request(method, url, pat, endpoint, payload=None):
nonlocal call_count
call_count += 1
if call_count == 1:
raise HTTPException(status_code=404, detail="not found")
if call_count == 2:
raise HTTPException(status_code=404, detail="not found on fallback")
return {"number": 1, "html_url": "http://fallback/pr/1", "state": "open"}
# _derive_server_url_from_remote returns different URL than primary
with patch.object(svc, "_gitea_request", side_effect=mock_request), \
patch.object(svc, "_derive_server_url_from_remote", return_value="http://fallback.example.com"), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value=None):
with pytest.raises(HTTPException):
await svc.create_gitea_pull_request(
"http://primary.example.com", "pat", "http://different.example.com/owner/repo.git",
"feature", "main", "PR title",
)
assert call_count == 2
# #endregion test_pr_404_retry_with_fallback
# #region test_pr_404_retry_with_branch_detail [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_pr_404_retry_with_branch_detail(self):
"""Fallback retry 404 → builds branch detail."""
svc = TestableGitGitea()
call_count = 0
async def mock_request(method, url, pat, endpoint, payload=None):
nonlocal call_count
call_count += 1
if call_count == 1:
raise HTTPException(status_code=404, detail="not found")
raise HTTPException(status_code=404, detail="not found on fallback")
with patch.object(svc, "_gitea_request", side_effect=mock_request), \
patch.object(svc, "_derive_server_url_from_remote", return_value="http://fallback.example.com"), \
patch.object(svc, "_build_gitea_pr_404_detail", new_callable=AsyncMock, return_value="Gitea branch not found: source branch 'feature' in owner/repo"):
with pytest.raises(HTTPException) as exc_info:
await svc.create_gitea_pull_request(
"http://primary.example.com", "pat", "http://different.example.com/owner/repo.git",
"feature", "main", "PR title",
)
assert exc_info.value.status_code == 400
assert "branch not found" in exc_info.value.detail
# #endregion test_pr_404_retry_with_branch_detail
# ── _build_gitea_pr_404_detail ──
class TestBuildGiteaPR404Detail:
"""_build_gitea_pr_404_detail — specific branch missing message."""
# #region test_source_branch_missing [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_source_branch_missing(self):
"""Source branch not found → specific message."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock, side_effect=[False, True]):
result = await svc._build_gitea_pr_404_detail(
"https://gitea.com", "pat", "owner", "repo", "feature", "main"
)
assert result is not None
assert "source branch" in result
assert "feature" in result
# #endregion test_source_branch_missing
# #region test_target_branch_missing [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_target_branch_missing(self):
"""Target branch not found → specific message."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_branch_exists", new_callable=AsyncMock, side_effect=[True, False]):
result = await svc._build_gitea_pr_404_detail(
"https://gitea.com", "pat", "owner", "repo", "feature", "main"
)
assert result is not None
assert "target branch" in result
assert "main" in result
# #endregion test_target_branch_missing
# #region test_both_branches_exist [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_both_branches_exist(self):
"""Both branches exist → returns None."""
svc = TestableGitGitea()
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
# #endregion test_both_branches_exist
# ── _gitea_request edge ──
class TestGiteaRequestEdge:
"""_gitea_request — error response handling edges."""
# #region test_request_error_json_parse_fallback [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_request_error_json_parse_fallback(self):
"""Error response with unparseable JSON → falls back to response.text."""
svc = TestableGitGitea()
resp = MagicMock()
resp.status_code = 500
resp.text = "Internal Server Error"
resp.json.side_effect = Exception("invalid json")
svc._http_client.request.return_value = resp
with pytest.raises(HTTPException, match="Gitea API error: Internal Server Error"):
await svc._gitea_request("GET", "https://gitea.com", "pat", "/user")
# #endregion test_request_error_json_parse_fallback
# ── create_gitea_repository edge ──
class TestCreateGiteaRepositoryEdge:
"""create_gitea_repository — optional params and raise paths."""
# #region test_create_repo_with_description [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_with_description(self):
"""Description and default_branch passed in payload."""
svc = TestableGitGitea()
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",
private=True, description="My repo", default_branch="main"
)
assert result["name"] == "my-repo"
call_kwargs = mock_req.call_args[1]
assert call_kwargs["payload"]["description"] == "My repo"
assert call_kwargs["payload"]["default_branch"] == "main"
# #endregion test_create_repo_with_description
# #region test_create_repo_conflict_non_dict_response [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_conflict_non_dict_response(self):
"""409 conflict but existing fetch returns non-dict → raises original."""
svc = TestableGitGitea()
call_count = 0
async def mock_request(method, url, pat, endpoint, payload=None):
nonlocal call_count
call_count += 1
if method == "POST":
raise HTTPException(status_code=409, detail="conflict")
if call_count <= 2:
return {"login": "user"}
return ["not", "a", "dict"] # non-dict from GET /repos/{owner}/{name}
with patch.object(svc, "_gitea_request", side_effect=mock_request):
with pytest.raises(HTTPException) as exc_info:
await svc.create_gitea_repository("https://gitea.com", "pat", "existing-repo")
assert exc_info.value.status_code == 409
# #endregion test_create_repo_conflict_non_dict_response
# #region test_create_repo_without_description [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_create_repo_without_description(self):
"""No description/default_branch → not in payload."""
svc = TestableGitGitea()
created = {"name": "simple-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", "simple-repo"
)
assert result["name"] == "simple-repo"
call_kwargs = mock_req.call_args[1]
assert "description" not in call_kwargs.get("payload", {})
# #endregion test_create_repo_without_description
# ── _gitea_branch_exists edge ──
class TestGiteaBranchExistsEdge:
"""_gitea_branch_exists — non-404 error re-raises."""
# #region test_branch_exists_raises_non_404 [C:2] [TYPE Function]
@pytest.mark.asyncio
async def test_branch_exists_raises_non_404(self):
"""Non-404 HTTPException → re-raised."""
svc = TestableGitGitea()
with patch.object(svc, "_gitea_request", side_effect=HTTPException(status_code=500, detail="server error")):
with pytest.raises(HTTPException, match="server error"):
await svc._gitea_branch_exists("https://gitea.com", "pat", "owner", "repo", "dev")
# #endregion test_branch_exists_raises_non_404
# #endregion Test.Git.Gitea