# #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