@@ -104,6 +104,103 @@ def call_api_side_effect(path, method, **kwargs):
104104 assert args [0 ] == '/repos/owner/repo/pulls/123/commits'
105105 assert kwargs .get ('auth_settings' ) == ['AuthorizationHeaderToken' ]
106106
107+
108+ @patch ('pr_agent.git_providers.gitea_provider.get_settings' )
109+ @patch ('pr_agent.git_providers.gitea_provider.giteapy.ApiClient' )
110+ def test_gitea_provider_preserves_non_utf8_text_file_content (self , mock_api_client_cls , mock_get_settings ):
111+ # Regression for the Qodo review on #2440: non-UTF-8 *text* (e.g. UTF-16)
112+ # must not be dropped to "" (which is indistinguishable from an empty file
113+ # and loses real content downstream). It is decoded via the shared
114+ # decode_if_bytes fallback chain instead of crashing or returning "".
115+ settings = MagicMock ()
116+ settings .get .side_effect = lambda k , d = None : {
117+ 'GITEA.URL' : 'https://gitea.example.com' ,
118+ 'GITEA.PERSONAL_ACCESS_TOKEN' : 'test-token' ,
119+ 'GITEA.REPO_SETTING' : None ,
120+ 'GITEA.SKIP_SSL_VERIFICATION' : False ,
121+ 'GITEA.SSL_CA_CERT' : None
122+ }.get (k , d )
123+ mock_get_settings .return_value = settings
124+
125+ mock_api_client = mock_api_client_cls .return_value
126+ mock_api_client .configuration .api_key = {'Authorization' : 'token test-token' }
127+ mock_resp = MagicMock ()
128+ # UTF-16-LE encoded text — not valid UTF-8, but legitimate text content.
129+ mock_resp .data = BytesIO ("hello world" .encode ("utf-16" ))
130+ mock_api_client .call_api .return_value = mock_resp
131+
132+ from pr_agent .git_providers .gitea_provider import RepoApi
133+
134+ repo_api = RepoApi (mock_api_client )
135+
136+ content = repo_api .get_file_content ('owner' , 'repo' , 'sha1' , 'notes.txt' )
137+ assert content != '' , "non-UTF-8 text must not be dropped to an empty string"
138+ assert all (ch in content for ch in "hello world" ), "the underlying text should survive the fallback decode"
139+ args , kwargs = mock_api_client .call_api .call_args
140+ assert args [0 ] == '/repos/owner/repo/raw/notes.txt'
141+ assert kwargs .get ('query_params' ) == [('ref' , 'sha1' )]
142+ assert kwargs .get ('auth_settings' ) == ['AuthorizationHeaderToken' ]
143+
144+ @patch ('pr_agent.git_providers.gitea_provider.get_settings' )
145+ @patch ('pr_agent.git_providers.gitea_provider.giteapy.ApiClient' )
146+ def test_gitea_provider_does_not_crash_on_binary_file_content (self , mock_api_client_cls , mock_get_settings ):
147+ # The original #2380 crash path: raw binary bytes must not raise
148+ # UnicodeDecodeError. decode_if_bytes yields a best-effort string; binary
149+ # files are filtered downstream by extension, so this only needs to not crash.
150+ settings = MagicMock ()
151+ settings .get .side_effect = lambda k , d = None : {
152+ 'GITEA.URL' : 'https://gitea.example.com' ,
153+ 'GITEA.PERSONAL_ACCESS_TOKEN' : 'test-token' ,
154+ 'GITEA.REPO_SETTING' : None ,
155+ 'GITEA.SKIP_SSL_VERIFICATION' : False ,
156+ 'GITEA.SSL_CA_CERT' : None
157+ }.get (k , d )
158+ mock_get_settings .return_value = settings
159+
160+ mock_api_client = mock_api_client_cls .return_value
161+ mock_api_client .configuration .api_key = {'Authorization' : 'token test-token' }
162+ mock_resp = MagicMock ()
163+ mock_resp .data = BytesIO (b'\xff \xd8 \xff \xe0 \x00 \x10 JFIF\x00 \x01 ' ) # JPEG header bytes
164+ mock_api_client .call_api .return_value = mock_resp
165+
166+ from pr_agent .git_providers .gitea_provider import RepoApi
167+
168+ repo_api = RepoApi (mock_api_client )
169+
170+ # Must not raise; result is a string (content filtered by extension downstream).
171+ assert isinstance (repo_api .get_file_content ('owner' , 'repo' , 'sha1' , 'assets/image.webp' ), str )
172+
173+
174+ @patch ('pr_agent.git_providers.gitea_provider.get_settings' )
175+ @patch ('pr_agent.git_providers.gitea_provider.giteapy.ApiClient' )
176+ def test_gitea_provider_decodes_non_utf8_diff_with_replacement (self , mock_api_client_cls , mock_get_settings ):
177+ settings = MagicMock ()
178+ settings .get .side_effect = lambda k , d = None : {
179+ 'GITEA.URL' : 'https://gitea.example.com' ,
180+ 'GITEA.PERSONAL_ACCESS_TOKEN' : 'test-token' ,
181+ 'GITEA.REPO_SETTING' : None ,
182+ 'GITEA.SKIP_SSL_VERIFICATION' : False ,
183+ 'GITEA.SSL_CA_CERT' : None
184+ }.get (k , d )
185+ mock_get_settings .return_value = settings
186+
187+ mock_api_client = mock_api_client_cls .return_value
188+ mock_api_client .configuration .api_key = {'Authorization' : 'token test-token' }
189+ mock_resp = MagicMock ()
190+ mock_resp .data = BytesIO (b'diff --git a/image.png b/image.webp\n +' + bytes ([0xff ]) + b'binary' )
191+ mock_api_client .call_api .return_value = mock_resp
192+
193+ from pr_agent .git_providers .gitea_provider import RepoApi
194+
195+ repo_api = RepoApi (mock_api_client )
196+
197+ diff = repo_api .get_pull_request_diff ('owner' , 'repo' , 123 )
198+
199+ assert 'diff --git a/image.png b/image.webp' in diff
200+ assert '�' in diff
201+ args , kwargs = mock_api_client .call_api .call_args
202+ assert args [0 ] == '/repos/owner/repo/pulls/123.diff'
203+ assert kwargs .get ('auth_settings' ) == ['AuthorizationHeaderToken' ]
107204 def test_get_repo_settings_returns_bytes (self ):
108205 """Regression for #2347: get_repo_settings must return bytes so that
109206 utils.apply_repo_settings can os.write() it and later .decode() it. The
0 commit comments