From 30e3011cb03f580700b40788ce322b9529632759 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:45:05 +0700 Subject: [PATCH] feat(backend): Include owner_type in the Get Repositories API response. (#9763) --- docs/openapi.json | 5 + .../bitbucket/bitbucket_service.py | 11 ++ .../integrations/github/github_service.py | 16 ++ .../integrations/gitlab/gitlab_service.py | 16 ++ openhands/integrations/service_types.py | 30 ++-- tests/unit/test_bitbucket.py | 163 ++++++++++++++++- tests/unit/test_github_service.py | 167 ++++++++++++++++- tests/unit/test_gitlab.py | 169 ++++++++++++++++++ 8 files changed, 564 insertions(+), 13 deletions(-) create mode 100644 tests/unit/test_gitlab.py diff --git a/docs/openapi.json b/docs/openapi.json index 954c289578..d36bdb8764 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1827,6 +1827,11 @@ "updated_at": { "type": "string", "format": "date-time" + }, + "owner_type": { + "type": "string", + "enum": ["user", "organization"], + "nullable": true } } }, diff --git a/openhands/integrations/bitbucket/bitbucket_service.py b/openhands/integrations/bitbucket/bitbucket_service.py index 5e8e36f93e..989c98069a 100644 --- a/openhands/integrations/bitbucket/bitbucket_service.py +++ b/openhands/integrations/bitbucket/bitbucket_service.py @@ -9,6 +9,7 @@ from openhands.integrations.service_types import ( BaseGitService, Branch, GitService, + OwnerType, ProviderType, Repository, RequestMethod, @@ -246,6 +247,11 @@ class BitBucketService(BaseGitService, GitService): is_public=repo.get('is_private', True) is False, stargazers_count=None, # Bitbucket doesn't have stars pushed_at=repo.get('updated_on'), + owner_type=( + OwnerType.ORGANIZATION + if repo.get('workspace', {}).get('is_private') is False + else OwnerType.USER + ), ) ) @@ -287,6 +293,11 @@ class BitBucketService(BaseGitService, GitService): is_public=data.get('is_private', True) is False, stargazers_count=None, # Bitbucket doesn't have stars pushed_at=data.get('updated_on'), + owner_type=( + OwnerType.ORGANIZATION + if data.get('workspace', {}).get('is_private') is False + else OwnerType.USER + ), ) async def get_branches(self, repository: str) -> list[Branch]: diff --git a/openhands/integrations/github/github_service.py b/openhands/integrations/github/github_service.py index c547166f68..def04bb11a 100644 --- a/openhands/integrations/github/github_service.py +++ b/openhands/integrations/github/github_service.py @@ -15,6 +15,7 @@ from openhands.integrations.service_types import ( BaseGitService, Branch, GitService, + OwnerType, ProviderType, Repository, RequestMethod, @@ -236,6 +237,11 @@ class GitHubService(BaseGitService, GitService): stargazers_count=repo.get('stargazers_count'), git_provider=ProviderType.GITHUB, is_public=not repo.get('private', True), + owner_type=( + OwnerType.ORGANIZATION + if repo.get('owner', {}).get('type') == 'Organization' + else OwnerType.USER + ), ) for repo in all_repos ] @@ -269,6 +275,11 @@ class GitHubService(BaseGitService, GitService): stargazers_count=repo.get('stargazers_count'), git_provider=ProviderType.GITHUB, is_public=True, + owner_type=( + OwnerType.ORGANIZATION + if repo.get('owner', {}).get('type') == 'Organization' + else OwnerType.USER + ), ) for repo in repo_items ] @@ -414,6 +425,11 @@ class GitHubService(BaseGitService, GitService): stargazers_count=repo.get('stargazers_count'), git_provider=ProviderType.GITHUB, is_public=not repo.get('private', True), + owner_type=( + OwnerType.ORGANIZATION + if repo.get('owner', {}).get('type') == 'Organization' + else OwnerType.USER + ), ) async def get_branches(self, repository: str) -> list[Branch]: diff --git a/openhands/integrations/gitlab/gitlab_service.py b/openhands/integrations/gitlab/gitlab_service.py index 78812c1003..ca70d629d3 100644 --- a/openhands/integrations/gitlab/gitlab_service.py +++ b/openhands/integrations/gitlab/gitlab_service.py @@ -8,6 +8,7 @@ from openhands.integrations.service_types import ( BaseGitService, Branch, GitService, + OwnerType, ProviderType, Repository, RequestMethod, @@ -214,6 +215,11 @@ class GitLabService(BaseGitService, GitService): stargazers_count=repo.get('star_count'), git_provider=ProviderType.GITLAB, is_public=True, + owner_type=( + OwnerType.ORGANIZATION + if repo.get('namespace', {}).get('kind') == 'group' + else OwnerType.USER + ), ) for repo in response ] @@ -265,6 +271,11 @@ class GitLabService(BaseGitService, GitService): stargazers_count=repo.get('star_count'), git_provider=ProviderType.GITLAB, is_public=repo.get('visibility') == 'public', + owner_type=( + OwnerType.ORGANIZATION + if repo.get('namespace', {}).get('kind') == 'group' + else OwnerType.USER + ), ) for repo in all_repos ] @@ -415,6 +426,11 @@ class GitLabService(BaseGitService, GitService): stargazers_count=repo.get('star_count'), git_provider=ProviderType.GITLAB, is_public=repo.get('visibility') == 'public', + owner_type=( + OwnerType.ORGANIZATION + if repo.get('namespace', {}).get('kind') == 'group' + else OwnerType.USER + ), ) async def get_branches(self, repository: str) -> list[Branch]: diff --git a/openhands/integrations/service_types.py b/openhands/integrations/service_types.py index 32b1eabfb0..05ce06cf90 100644 --- a/openhands/integrations/service_types.py +++ b/openhands/integrations/service_types.py @@ -24,6 +24,11 @@ class TaskType(str, Enum): OPEN_PR = 'OPEN_PR' +class OwnerType(str, Enum): + USER = 'user' + ORGANIZATION = 'organization' + + class SuggestedTask(BaseModel): git_provider: ProviderType task_type: TaskType @@ -32,17 +37,7 @@ class SuggestedTask(BaseModel): title: str def get_provider_terms(self) -> dict: - if self.git_provider == ProviderType.GITLAB: - return { - 'requestType': 'Merge Request', - 'requestTypeShort': 'MR', - 'apiName': 'GitLab API', - 'tokenEnvVar': 'GITLAB_TOKEN', - 'ciSystem': 'CI pipelines', - 'ciProvider': 'GitLab', - 'requestVerb': 'merge request', - } - elif self.git_provider == ProviderType.GITHUB: + if self.git_provider == ProviderType.GITHUB: return { 'requestType': 'Pull Request', 'requestTypeShort': 'PR', @@ -52,6 +47,16 @@ class SuggestedTask(BaseModel): 'ciProvider': 'GitHub', 'requestVerb': 'pull request', } + elif self.git_provider == ProviderType.GITLAB: + return { + 'requestType': 'Merge Request', + 'requestTypeShort': 'MR', + 'apiName': 'GitLab API', + 'tokenEnvVar': 'GITLAB_TOKEN', + 'ciSystem': 'CI pipelines', + 'ciProvider': 'GitLab', + 'requestVerb': 'merge request', + } elif self.git_provider == ProviderType.BITBUCKET: return { 'requestType': 'Pull Request', @@ -117,6 +122,9 @@ class Repository(BaseModel): stargazers_count: int | None = None link_header: str | None = None pushed_at: str | None = None # ISO 8601 format date string + owner_type: OwnerType | None = ( + None # Whether the repository is owned by a user or organization + ) class AuthenticationError(ValueError): diff --git a/tests/unit/test_bitbucket.py b/tests/unit/test_bitbucket.py index 3df283d229..6e7bf91a87 100644 --- a/tests/unit/test_bitbucket.py +++ b/tests/unit/test_bitbucket.py @@ -9,8 +9,8 @@ from pydantic import SecretStr from openhands.integrations.bitbucket.bitbucket_service import BitBucketService from openhands.integrations.provider import ProviderToken, ProviderType +from openhands.integrations.service_types import OwnerType, Repository from openhands.integrations.service_types import ProviderType as ServiceProviderType -from openhands.integrations.service_types import Repository from openhands.integrations.utils import validate_provider_token from openhands.resolver.interfaces.bitbucket import BitbucketIssueHandler from openhands.resolver.interfaces.issue import Issue @@ -592,6 +592,167 @@ async def test_validate_provider_token_with_empty_tokens(): assert result is None +@pytest.mark.asyncio +async def test_bitbucket_get_repositories_with_user_owner_type(): + """Test that get_repositories correctly sets owner_type field for user repositories.""" + service = BitBucketService(token=SecretStr('test-token')) + + # Mock repository data for user repositories (private workspace) + mock_workspaces = [{'slug': 'test-user', 'name': 'Test User'}] + mock_repos = [ + { + 'uuid': 'repo-1', + 'slug': 'user-repo1', + 'workspace': {'slug': 'test-user', 'is_private': True}, + 'is_private': False, + 'updated_on': '2023-01-01T00:00:00Z', + }, + { + 'uuid': 'repo-2', + 'slug': 'user-repo2', + 'workspace': {'slug': 'test-user', 'is_private': True}, + 'is_private': True, + 'updated_on': '2023-01-02T00:00:00Z', + }, + ] + + with patch.object(service, '_fetch_paginated_data') as mock_fetch: + mock_fetch.side_effect = [mock_workspaces, mock_repos] + + repositories = await service.get_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 (private workspace) + for repo in repositories: + assert repo.owner_type == OwnerType.USER + assert isinstance(repo, Repository) + assert repo.git_provider == ServiceProviderType.BITBUCKET + + +@pytest.mark.asyncio +async def test_bitbucket_get_repositories_with_organization_owner_type(): + """Test that get_repositories correctly sets owner_type field for organization repositories.""" + service = BitBucketService(token=SecretStr('test-token')) + + # Mock repository data for organization repositories (public workspace) + mock_workspaces = [{'slug': 'test-org', 'name': 'Test Organization'}] + mock_repos = [ + { + 'uuid': 'repo-3', + 'slug': 'org-repo1', + 'workspace': {'slug': 'test-org', 'is_private': False}, + 'is_private': False, + 'updated_on': '2023-01-03T00:00:00Z', + }, + { + 'uuid': 'repo-4', + 'slug': 'org-repo2', + 'workspace': {'slug': 'test-org', 'is_private': False}, + 'is_private': True, + 'updated_on': '2023-01-04T00:00:00Z', + }, + ] + + with patch.object(service, '_fetch_paginated_data') as mock_fetch: + mock_fetch.side_effect = [mock_workspaces, mock_repos] + + repositories = await service.get_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 (public workspace) + for repo in repositories: + assert repo.owner_type == OwnerType.ORGANIZATION + assert isinstance(repo, Repository) + assert repo.git_provider == ServiceProviderType.BITBUCKET + + +@pytest.mark.asyncio +async def test_bitbucket_get_repositories_mixed_owner_types(): + """Test that get_repositories correctly handles mixed user and organization repositories.""" + service = BitBucketService(token=SecretStr('test-token')) + + # Mock repository data with mixed workspace types + mock_workspaces = [ + {'slug': 'test-user', 'name': 'Test User'}, + {'slug': 'test-org', 'name': 'Test Organization'}, + ] + + # First workspace (user) repositories + mock_user_repos = [ + { + 'uuid': 'repo-1', + 'slug': 'user-repo', + 'workspace': {'slug': 'test-user', 'is_private': True}, + 'is_private': False, + 'updated_on': '2023-01-01T00:00:00Z', + } + ] + + # Second workspace (organization) repositories + mock_org_repos = [ + { + 'uuid': 'repo-2', + 'slug': 'org-repo', + 'workspace': {'slug': 'test-org', 'is_private': False}, + 'is_private': False, + 'updated_on': '2023-01-02T00:00:00Z', + } + ] + + with patch.object(service, '_fetch_paginated_data') as mock_fetch: + mock_fetch.side_effect = [mock_workspaces, mock_user_repos, mock_org_repos] + + repositories = await service.get_repositories('pushed', AppMode.SAAS) + + # Verify we got repositories from both workspaces + 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_bitbucket_get_repositories_owner_type_fallback(): + """Test that owner_type defaults to USER when workspace is private.""" + service = BitBucketService(token=SecretStr('test-token')) + + # Mock repository data with private workspace (should default to USER) + mock_workspaces = [{'slug': 'test-user', 'name': 'Test User'}] + mock_repos = [ + { + 'uuid': 'repo-1', + 'slug': 'user-repo', + 'workspace': {'slug': 'test-user', 'is_private': True}, # Private workspace + 'is_private': False, + 'updated_on': '2023-01-01T00:00:00Z', + }, + { + 'uuid': 'repo-2', + 'slug': 'another-user-repo', + 'workspace': {'slug': 'test-user', 'is_private': True}, # Private workspace + 'is_private': True, + 'updated_on': '2023-01-02T00:00:00Z', + }, + ] + + with patch.object(service, '_fetch_paginated_data') as mock_fetch: + mock_fetch.side_effect = [mock_workspaces, mock_repos] + + repositories = await service.get_repositories('pushed', AppMode.SAAS) + + # Verify all repositories default to USER owner_type for private workspaces + for repo in repositories: + assert repo.owner_type == OwnerType.USER + + # Setup.py Bitbucket Token Tests @patch('openhands.core.setup.call_async_from_sync') @patch('openhands.core.setup.get_file_store') diff --git a/tests/unit/test_github_service.py b/tests/unit/test_github_service.py index 0b88a96b44..328c67598b 100644 --- a/tests/unit/test_github_service.py +++ b/tests/unit/test_github_service.py @@ -5,7 +5,13 @@ import pytest from pydantic import SecretStr from openhands.integrations.github.github_service import GitHubService -from openhands.integrations.service_types import AuthenticationError +from openhands.integrations.service_types import ( + AuthenticationError, + OwnerType, + ProviderType, + Repository, +) +from openhands.server.types import AppMode @pytest.mark.asyncio @@ -79,3 +85,162 @@ async def test_github_service_fetch_data(): with pytest.raises(AuthenticationError): _ = await service._make_request('https://api.github.com/user') + + +@pytest.mark.asyncio +async def test_github_get_repositories_with_user_owner_type(): + """Test that get_repositories correctly sets owner_type field for user repositories.""" + service = GitHubService(user_id=None, token=SecretStr('test-token')) + + # Mock repository data for user repositories + mock_repo_data = [ + { + 'id': 123, + 'full_name': 'test-user/test-repo', + 'private': False, + 'stargazers_count': 10, + 'owner': {'type': 'User'}, # User repository + }, + { + 'id': 456, + 'full_name': 'test-user/another-repo', + 'private': True, + 'stargazers_count': 5, + 'owner': {'type': 'User'}, # User repository + }, + ] + + with ( + patch.object(service, '_fetch_paginated_repos', return_value=mock_repo_data), + patch.object(service, 'get_installation_ids', return_value=[123]), + ): + repositories = await service.get_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.GITHUB + + +@pytest.mark.asyncio +async def test_github_get_repositories_with_organization_owner_type(): + """Test that get_repositories correctly sets owner_type field for organization repositories.""" + service = GitHubService(user_id=None, token=SecretStr('test-token')) + + # Mock repository data for organization repositories + mock_repo_data = [ + { + 'id': 789, + 'full_name': 'test-org/org-repo', + 'private': False, + 'stargazers_count': 25, + 'owner': {'type': 'Organization'}, # Organization repository + }, + { + 'id': 101, + 'full_name': 'test-org/another-org-repo', + 'private': True, + 'stargazers_count': 15, + 'owner': {'type': 'Organization'}, # Organization repository + }, + ] + + with ( + patch.object(service, '_fetch_paginated_repos', return_value=mock_repo_data), + patch.object(service, 'get_installation_ids', return_value=[123]), + ): + repositories = await service.get_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.GITHUB + + +@pytest.mark.asyncio +async def test_github_get_repositories_mixed_owner_types(): + """Test that get_repositories correctly handles mixed user and organization repositories.""" + service = GitHubService(user_id=None, token=SecretStr('test-token')) + + # Mock repository data with mixed owner types + mock_repo_data = [ + { + 'id': 123, + 'full_name': 'test-user/user-repo', + 'private': False, + 'stargazers_count': 10, + 'owner': {'type': 'User'}, # User repository + }, + { + 'id': 456, + 'full_name': 'test-org/org-repo', + 'private': True, + 'stargazers_count': 25, + 'owner': {'type': 'Organization'}, # Organization repository + }, + ] + + with ( + patch.object(service, '_fetch_paginated_repos', return_value=mock_repo_data), + patch.object(service, 'get_installation_ids', return_value=[123]), + ): + repositories = await service.get_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_github_get_repositories_owner_type_fallback(): + """Test that owner_type defaults to USER when owner type is not 'Organization'.""" + service = GitHubService(user_id=None, token=SecretStr('test-token')) + + # Mock repository data with missing or unexpected owner type + mock_repo_data = [ + { + 'id': 123, + 'full_name': 'test-user/test-repo', + 'private': False, + 'stargazers_count': 10, + 'owner': {'type': 'User'}, # Explicitly User + }, + { + 'id': 456, + 'full_name': 'test-user/another-repo', + 'private': True, + 'stargazers_count': 5, + 'owner': {'type': 'Bot'}, # Unexpected type + }, + { + 'id': 789, + 'full_name': 'test-user/third-repo', + 'private': False, + 'stargazers_count': 15, + 'owner': {}, # Missing type + }, + ] + + with ( + patch.object(service, '_fetch_paginated_repos', return_value=mock_repo_data), + patch.object(service, 'get_installation_ids', return_value=[123]), + ): + repositories = await service.get_repositories('pushed', AppMode.SAAS) + + # Verify all repositories default to USER owner_type + for repo in repositories: + assert repo.owner_type == OwnerType.USER diff --git a/tests/unit/test_gitlab.py b/tests/unit/test_gitlab.py new file mode 100644 index 0000000000..57776e06cb --- /dev/null +++ b/tests/unit/test_gitlab.py @@ -0,0 +1,169 @@ +"""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_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_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_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_repositories('pushed', AppMode.SAAS) + + # Verify all repositories default to USER owner_type + for repo in repositories: + assert repo.owner_type == OwnerType.USER