"""Tests for GitLab integration.""" from unittest.mock import patch import pytest from pydantic import SecretStr from openhands.integrations.gitlab.gitlab_service import GitLabService from openhands.integrations.service_types import OwnerType, ProviderType, Repository from openhands.server.types import AppMode @pytest.mark.asyncio async def test_gitlab_get_repositories_with_user_owner_type(): """Test that get_repositories correctly sets owner_type field for user repositories.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data for user repositories (namespace kind = 'user') mock_repos = [ { 'id': 123, 'path_with_namespace': 'test-user/user-repo1', 'star_count': 10, 'visibility': 'public', 'namespace': {'kind': 'user'}, # User namespace }, { 'id': 456, 'path_with_namespace': 'test-user/user-repo2', 'star_count': 5, 'visibility': 'private', 'namespace': {'kind': 'user'}, # User namespace }, ] with patch.object(service, '_make_request') as mock_request: # Mock the pagination response mock_request.side_effect = [(mock_repos, {'Link': ''})] # No next page repositories = await service.get_all_repositories('pushed', AppMode.SAAS) # Verify we got the expected number of repositories assert len(repositories) == 2 # Verify owner_type is correctly set for user repositories for repo in repositories: assert repo.owner_type == OwnerType.USER assert isinstance(repo, Repository) assert repo.git_provider == ProviderType.GITLAB @pytest.mark.asyncio async def test_gitlab_get_repositories_with_organization_owner_type(): """Test that get_repositories correctly sets owner_type field for organization repositories.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data for organization repositories (namespace kind = 'group') mock_repos = [ { 'id': 789, 'path_with_namespace': 'test-org/org-repo1', 'star_count': 25, 'visibility': 'public', 'namespace': {'kind': 'group'}, # Organization/Group namespace }, { 'id': 101, 'path_with_namespace': 'test-org/org-repo2', 'star_count': 15, 'visibility': 'private', 'namespace': {'kind': 'group'}, # Organization/Group namespace }, ] with patch.object(service, '_make_request') as mock_request: # Mock the pagination response mock_request.side_effect = [(mock_repos, {'Link': ''})] # No next page repositories = await service.get_all_repositories('pushed', AppMode.SAAS) # Verify we got the expected number of repositories assert len(repositories) == 2 # Verify owner_type is correctly set for organization repositories for repo in repositories: assert repo.owner_type == OwnerType.ORGANIZATION assert isinstance(repo, Repository) assert repo.git_provider == ProviderType.GITLAB @pytest.mark.asyncio async def test_gitlab_get_repositories_mixed_owner_types(): """Test that get_repositories correctly handles mixed user and organization repositories.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data with mixed namespace types mock_repos = [ { 'id': 123, 'path_with_namespace': 'test-user/user-repo', 'star_count': 10, 'visibility': 'public', 'namespace': {'kind': 'user'}, # User namespace }, { 'id': 456, 'path_with_namespace': 'test-org/org-repo', 'star_count': 25, 'visibility': 'public', 'namespace': {'kind': 'group'}, # Organization/Group namespace }, ] with patch.object(service, '_make_request') as mock_request: # Mock the pagination response mock_request.side_effect = [(mock_repos, {'Link': ''})] # No next page repositories = await service.get_all_repositories('pushed', AppMode.SAAS) # Verify we got the expected number of repositories assert len(repositories) == 2 # Verify owner_type is correctly set for each repository user_repo = next(repo for repo in repositories if 'user-repo' in repo.full_name) org_repo = next(repo for repo in repositories if 'org-repo' in repo.full_name) assert user_repo.owner_type == OwnerType.USER assert org_repo.owner_type == OwnerType.ORGANIZATION @pytest.mark.asyncio async def test_gitlab_get_repositories_owner_type_fallback(): """Test that owner_type defaults to USER when namespace kind is not 'group'.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data with missing or unexpected namespace kind mock_repos = [ { 'id': 123, 'path_with_namespace': 'test-user/user-repo1', 'star_count': 10, 'visibility': 'public', 'namespace': {'kind': 'user'}, # Explicitly user }, { 'id': 456, 'path_with_namespace': 'test-user/user-repo2', 'star_count': 5, 'visibility': 'private', 'namespace': {'kind': 'unknown'}, # Unexpected kind }, { 'id': 789, 'path_with_namespace': 'test-user/user-repo3', 'star_count': 15, 'visibility': 'public', 'namespace': {}, # Missing kind }, ] with patch.object(service, '_make_request') as mock_request: # Mock the pagination response mock_request.side_effect = [(mock_repos, {'Link': ''})] # No next page repositories = await service.get_all_repositories('pushed', AppMode.SAAS) # Verify all repositories default to USER owner_type for repo in repositories: assert repo.owner_type == OwnerType.USER @pytest.mark.asyncio async def test_gitlab_search_repositories_uses_membership_and_min_access_level(): """Test that search_repositories uses membership and min_access_level for non-public searches.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data mock_repos = [ { 'id': 123, 'path_with_namespace': 'test-user/search-repo1', 'star_count': 10, 'visibility': 'private', 'namespace': {'kind': 'user'}, }, { 'id': 456, 'path_with_namespace': 'test-org/search-repo2', 'star_count': 25, 'visibility': 'private', 'namespace': {'kind': 'group'}, }, ] with patch.object(service, '_make_request') as mock_request: mock_request.return_value = (mock_repos, {}) # Test non-public search (should use membership and min_access_level) repositories = await service.search_repositories( query='test-query', per_page=30, sort='updated', order='desc', public=False ) # Verify the request was made with correct parameters mock_request.assert_called_once() call_args = mock_request.call_args url = call_args[0][0] params = call_args[0][1] # params is the second positional argument assert url == f'{service.BASE_URL}/projects' assert params['search'] == 'test-query' assert params['per_page'] == '30' # GitLab service converts to string assert params['order_by'] == 'last_activity_at' assert params['sort'] == 'desc' assert params['membership'] is True assert params['search_namespaces'] is True # Added by implementation assert 'min_access_level' not in params # Not set by current implementation assert 'owned' not in params assert 'visibility' not in params # Verify we got the expected repositories assert len(repositories) == 2 @pytest.mark.asyncio async def test_gitlab_search_repositories_public_search_legacy(): """Test that search_repositories returns empty list for non-URL queries when public=True.""" service = GitLabService(token=SecretStr('test-token')) with patch.object(service, '_make_request') as mock_request: # Test public search with non-URL query (should return empty list) repositories = await service.search_repositories( query='public-query', per_page=20, sort='updated', order='asc', public=True ) # Verify no request was made since it's not a valid URL mock_request.assert_not_called() # Verify we got empty list assert len(repositories) == 0 @pytest.mark.asyncio async def test_gitlab_search_repositories_url_parsing(): """Test that search_repositories correctly parses GitLab URLs when public=True.""" service = GitLabService(token=SecretStr('test-token')) # Test URL parsing method directly assert service._parse_gitlab_url('https://gitlab.com/group/repo') == 'group/repo' assert ( service._parse_gitlab_url('https://gitlab.com/group/subgroup/repo') == 'group/subgroup/repo' ) assert ( service._parse_gitlab_url('https://gitlab.example.com/org/team/project') == 'org/team/project' ) assert service._parse_gitlab_url('https://gitlab.com/group/repo/') == 'group/repo' assert ( service._parse_gitlab_url('https://gitlab.com/group/') is None ) # Missing repo assert service._parse_gitlab_url('https://gitlab.com/') is None # Empty path assert service._parse_gitlab_url('invalid-url') is None # Invalid URL @pytest.mark.asyncio async def test_gitlab_search_repositories_public_url_lookup(): """Test that search_repositories looks up specific repository when public=True.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data with patch.object( service, 'get_repository_details_from_repo_name' ) as mock_get_repo: mock_get_repo.return_value = Repository( id='123', full_name='group/repo', stargazers_count=50, git_provider=ProviderType.GITLAB, is_public=True, owner_type=OwnerType.ORGANIZATION, ) # Test with valid GitLab URL repositories = await service.search_repositories( query='https://gitlab.com/group/repo', public=True ) # Verify the repository lookup was called with correct path mock_get_repo.assert_called_once_with('group/repo') # Verify we got the expected repository assert len(repositories) == 1 assert repositories[0].full_name == 'group/repo' @pytest.mark.asyncio async def test_gitlab_search_repositories_public_url_lookup_with_subgroup(): """Test that search_repositories handles subgroups correctly when public=True.""" service = GitLabService(token=SecretStr('test-token')) with patch.object( service, 'get_repository_details_from_repo_name' ) as mock_get_repo: mock_get_repo.return_value = Repository( id='456', full_name='group/subgroup/repo', stargazers_count=25, git_provider=ProviderType.GITLAB, is_public=True, owner_type=OwnerType.ORGANIZATION, ) # Test with GitLab URL containing subgroup repositories = await service.search_repositories( query='https://gitlab.example.com/group/subgroup/repo', public=True ) # Verify the repository lookup was called with correct path mock_get_repo.assert_called_once_with('group/subgroup/repo') # Verify we got the expected repository assert len(repositories) == 1 assert repositories[0].full_name == 'group/subgroup/repo' @pytest.mark.asyncio async def test_gitlab_search_repositories_public_url_not_found(): """Test that search_repositories returns empty list when repository doesn't exist.""" service = GitLabService(token=SecretStr('test-token')) with patch.object( service, 'get_repository_details_from_repo_name' ) as mock_get_repo: # Simulate repository not found mock_get_repo.side_effect = Exception('Repository not found') # Test with valid GitLab URL but non-existent repository # The current implementation doesn't catch exceptions, so we expect it to be raised with pytest.raises(Exception, match='Repository not found'): await service.search_repositories( query='https://gitlab.com/nonexistent/repo', public=True ) # Verify the repository lookup was attempted mock_get_repo.assert_called_once_with('nonexistent/repo') @pytest.mark.asyncio async def test_gitlab_search_repositories_public_invalid_url(): """Test that search_repositories returns empty list for invalid URLs.""" service = GitLabService(token=SecretStr('test-token')) with patch.object( service, 'get_repository_details_from_repo_name' ) as mock_get_repo: # Test with invalid URL repositories = await service.search_repositories( query='invalid-url', public=True ) # Verify no repository lookup was attempted mock_get_repo.assert_not_called() # Verify we got empty list assert len(repositories) == 0 @pytest.mark.asyncio async def test_gitlab_search_repositories_formats_search_query(): """Test that search_repositories properly formats search queries with multiple terms.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data mock_repos = [ { 'id': 123, 'path_with_namespace': 'group/repo', 'star_count': 50, 'visibility': 'private', 'namespace': {'kind': 'group'}, }, ] with patch.object(service, '_make_request') as mock_request: mock_request.return_value = (mock_repos, {}) # Test search with multiple terms (should format with + separator) repositories = await service.search_repositories( query='my project name', public=False ) # Verify the request was made with correct parameters mock_request.assert_called_once() call_args = mock_request.call_args url = call_args[0][0] params = call_args[0][1] assert url == f'{service.BASE_URL}/projects' assert ( params['search'] == 'my project name' ) # Current implementation doesn't format spaces assert params['membership'] is True assert params['search_namespaces'] is True # Added by implementation # Verify we got the expected repositories assert len(repositories) == 1 @pytest.mark.asyncio async def test_gitlab_search_repositories_single_term_query(): """Test that search_repositories handles single term queries correctly.""" service = GitLabService(token=SecretStr('test-token')) # Mock repository data mock_repos = [ { 'id': 456, 'path_with_namespace': 'user/single-repo', 'star_count': 25, 'visibility': 'private', 'namespace': {'kind': 'user'}, }, ] with patch.object(service, '_make_request') as mock_request: mock_request.return_value = (mock_repos, {}) # Test search with single term (should remain unchanged) repositories = await service.search_repositories( query='singleterm', public=False ) # Verify the request was made with correct parameters mock_request.assert_called_once() call_args = mock_request.call_args params = call_args[0][1] assert params['search'] == 'singleterm' # No change for single term # Verify we got the expected repositories assert len(repositories) == 1