Skip to content

Commit 5ee4393

Browse files
ilia-cyclaude
andcommitted
CM-67391: Fix secret presigned upload test mocks and harden fallback
Switching secret scans to the presigned upload path routed them through get_upload_link -> S3 -> scan_repository_from_upload_id, none of which were registered by mock_scan_async_responses. The scan hit an unmocked endpoint, returned 0 violations, and failed test_passing_output_option. - Extend mock_scan_async_responses to register the presigned endpoints (upload-link, S3 POST, repository) for presigned scan types, branching on should_use_presigned_upload. - Widen the presigned-upload fallback in _perform_scan to also catch the client's wrapped RequestError/SlowUploadConnectionError, not just raw requests.RequestException. A connection/timeout error from get_upload_link or the scan trigger otherwise never fell back to the Cycode-API upload. - Add a regression test covering the wrapped-exception fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9ea5f3a commit 5ee4393

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

cycode/cli/apps/scan/code_scanner.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,11 @@ def _perform_scan(
391391
is_commit_range,
392392
on_upload_progress,
393393
)
394-
except requests.exceptions.RequestException:
394+
except (
395+
requests.exceptions.RequestException,
396+
custom_exceptions.RequestError,
397+
custom_exceptions.SlowUploadConnectionError,
398+
):
395399
logger.warning('Direct upload to object storage failed. Falling back to upload via Cycode API. ')
396400

397401
return _perform_scan_async(

tests/cli/commands/scan/test_code_scanner.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import pytest
66

77
from cycode.cli import consts
8-
from cycode.cli.apps.scan.code_scanner import scan_disk_files, scan_documents
8+
from cycode.cli.apps.scan.code_scanner import _perform_scan, scan_disk_files, scan_documents
9+
from cycode.cli.exceptions import custom_exceptions
910
from cycode.cli.files_collector.file_excluder import _is_file_relevant_for_sca_scan
1011
from cycode.cli.files_collector.path_documents import _generate_document
1112
from cycode.cli.models import Document
@@ -212,3 +213,27 @@ def test_scan_documents_routes_upload_by_scan_type_and_sync(
212213

213214
assert mock_presigned_upload.called is expect_presigned
214215
assert mock_batched_scan.called is (not expect_presigned)
216+
217+
218+
@patch('cycode.cli.apps.scan.code_scanner._perform_scan_async')
219+
@patch('cycode.cli.apps.scan.code_scanner._perform_scan_v4_async')
220+
def test_perform_scan_falls_back_to_api_when_presigned_upload_raises_wrapped_error(
221+
mock_v4_async: Mock, mock_async: Mock
222+
) -> None:
223+
# RequestConnectionError is a CycodeError, not a requests.RequestException — the fallback must still catch it.
224+
mock_v4_async.side_effect = custom_exceptions.RequestConnectionError
225+
fallback_result = object()
226+
mock_async.return_value = fallback_result
227+
228+
result = _perform_scan(
229+
cycode_client=MagicMock(),
230+
zipped_documents=MagicMock(),
231+
scan_type=consts.SAST_SCAN_TYPE,
232+
is_git_diff=False,
233+
is_commit_range=False,
234+
scan_parameters={},
235+
)
236+
237+
assert result is fallback_result
238+
mock_v4_async.assert_called_once()
239+
mock_async.assert_called_once()

tests/cyclient/mocked_responses/scan_client.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import responses
77

8+
from cycode.cli.utils.scan_utils import should_use_presigned_upload
89
from cycode.cyclient.scan_client import ScanClient
910
from tests.conftest import MOCKED_RESPONSES_PATH
1011

@@ -128,6 +129,38 @@ def get_scan_configuration_response(url: str) -> responses.Response:
128129
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
129130

130131

132+
_PRESIGNED_UPLOAD_URL = 'https://cycode-tests.s3.amazonaws.com/presigned-upload'
133+
134+
135+
def get_upload_link_url(scan_type: str, scan_client: ScanClient) -> str:
136+
api_url = scan_client.scan_cycode_client.api_url
137+
async_scan_type = scan_client.scan_config.get_async_scan_type(scan_type)
138+
service_url = f'{scan_client.get_scan_service_v4_url_path(scan_type)}/{async_scan_type}/upload-link'
139+
return f'{api_url}/{service_url}'
140+
141+
142+
def get_upload_link_response(url: str) -> responses.Response:
143+
json_response = {'upload_id': str(uuid4()), 'url': _PRESIGNED_UPLOAD_URL, 'presigned_post_fields': {}}
144+
return responses.Response(method=responses.GET, url=url, json=json_response, status=200)
145+
146+
147+
def get_presigned_upload_response() -> responses.Response:
148+
return responses.Response(method=responses.POST, url=_PRESIGNED_UPLOAD_URL, status=204)
149+
150+
151+
def get_scan_from_upload_id_url(scan_type: str, scan_client: ScanClient) -> str:
152+
api_url = scan_client.scan_cycode_client.api_url
153+
async_scan_type = scan_client.scan_config.get_async_scan_type(scan_type)
154+
service_url = f'{scan_client.get_scan_service_v4_url_path(scan_type)}/{async_scan_type}/repository'
155+
return f'{api_url}/{service_url}'
156+
157+
158+
def get_scan_from_upload_id_response(url: str, scan_id: Optional[UUID] = None) -> responses.Response:
159+
if not scan_id:
160+
scan_id = uuid4()
161+
return responses.Response(method=responses.POST, url=url, json={'scan_id': str(scan_id)}, status=200)
162+
163+
131164
def mock_remote_config_responses(responses_module: responses, scan_type: str, scan_client: ScanClient) -> None:
132165
responses_module.add(get_scan_configuration_response(get_scan_configuration_url(scan_type, scan_client)))
133166

@@ -136,9 +169,18 @@ def mock_scan_async_responses(
136169
responses_module: responses, scan_type: str, scan_client: ScanClient, scan_id: UUID, zip_content_path: Path
137170
) -> None:
138171
mock_remote_config_responses(responses_module, scan_type, scan_client)
139-
responses_module.add(
140-
get_zipped_file_scan_async_response(get_zipped_file_scan_async_url(scan_type, scan_client), scan_id)
141-
)
172+
173+
if should_use_presigned_upload(scan_type):
174+
responses_module.add(get_upload_link_response(get_upload_link_url(scan_type, scan_client)))
175+
responses_module.add(get_presigned_upload_response())
176+
responses_module.add(
177+
get_scan_from_upload_id_response(get_scan_from_upload_id_url(scan_type, scan_client), scan_id)
178+
)
179+
else:
180+
responses_module.add(
181+
get_zipped_file_scan_async_response(get_zipped_file_scan_async_url(scan_type, scan_client), scan_id)
182+
)
183+
142184
responses_module.add(get_scan_details_response(get_scan_details_url(scan_type, scan_id, scan_client), scan_id))
143185
responses_module.add(get_detection_rules_response(get_detection_rules_url(scan_client)))
144186
responses_module.add(get_scan_detections_response(get_scan_detections_url(scan_client), scan_id, zip_content_path))

0 commit comments

Comments
 (0)