diff --git a/pr_agent/git_providers/gitea_provider.py b/pr_agent/git_providers/gitea_provider.py index 89a6248e9b..a00be44caa 100644 --- a/pr_agent/git_providers/gitea_provider.py +++ b/pr_agent/git_providers/gitea_provider.py @@ -605,11 +605,11 @@ def get_pr_labels(self,update=False) -> List[str]: return [label.name for label in labels] - def get_repo_settings(self) -> str: + def get_repo_settings(self) -> bytes: """Get repository settings""" if not self.repo_settings: self.logger.error("Repository settings not found") - return "" + return b"" response = self.repo_api.get_file_content( owner=self.owner, @@ -619,9 +619,13 @@ def get_repo_settings(self) -> str: ) if not response: self.logger.error("Failed to get repository settings") - return "" + return b"" - return response + # utils.apply_repo_settings() writes this via os.write() and later + # calls .decode() on it, so it must be bytes to match the GitHub/ + # GitLab/Bitbucket contract. get_file_content() decodes the raw bytes + # to str, so re-encode here (see issue #2347). + return response.encode('utf-8') def get_user_id(self) -> str: """Get the ID of the authenticated user""" diff --git a/tests/unittest/test_gitea_provider.py b/tests/unittest/test_gitea_provider.py index 4174b398d0..f3d2bc0620 100644 --- a/tests/unittest/test_gitea_provider.py +++ b/tests/unittest/test_gitea_provider.py @@ -103,3 +103,48 @@ def call_api_side_effect(path, method, **kwargs): args, kwargs = mock_api_client.call_api.call_args assert args[0] == '/repos/owner/repo/pulls/123/commits' assert kwargs.get('auth_settings') == ['AuthorizationHeaderToken'] + + def test_get_repo_settings_returns_bytes(self): + """Regression for #2347: get_repo_settings must return bytes so that + utils.apply_repo_settings can os.write() it and later .decode() it. The + Gitea raw-file API yields str (unlike GitHub/GitLab/Bitbucket, which hand + back bytes), so the provider must encode before returning.""" + from pr_agent.git_providers.gitea_provider import GiteaProvider + + toml = '[pr_reviewer]\nnum_code_suggestions = 4\n' + provider = GiteaProvider.__new__(GiteaProvider) + provider.logger = MagicMock() + provider.owner = 'owner' + provider.repo = 'repo' + provider.sha = 'sha1' + provider.repo_settings = '.pr_agent.toml' + provider.repo_api = MagicMock() + provider.repo_api.get_file_content.return_value = toml # API decodes to str + + result = provider.get_repo_settings() + + assert isinstance(result, bytes) + assert result == toml.encode('utf-8') + # The bytes must survive the exact operations utils.py performs on them. + assert result.decode() == toml + + def test_get_repo_settings_empty_bytes_when_unset_or_missing(self): + """No settings path configured, or empty/absent file: return empty + bytes, so every code path honours the -> bytes contract (not just the + success path) and a caller can never receive a str.""" + from pr_agent.git_providers.gitea_provider import GiteaProvider + + unset = GiteaProvider.__new__(GiteaProvider) + unset.logger = MagicMock() + unset.repo_settings = None + assert unset.get_repo_settings() == b"" + + empty = GiteaProvider.__new__(GiteaProvider) + empty.logger = MagicMock() + empty.owner = 'owner' + empty.repo = 'repo' + empty.sha = 'sha1' + empty.repo_settings = '.pr_agent.toml' + empty.repo_api = MagicMock() + empty.repo_api.get_file_content.return_value = '' + assert empty.get_repo_settings() == b""