Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cumulusci/salesforce_api/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@ def __call__(self):
if self.status != "Failed":
try:
return self._process_response(response)
except (MetadataApiError, ApexTestException):
raise
except Exception as e:
raise MetadataParseError(
f"Could not process MDAPI response: {str(e)}", response=response
)
) from e
else:
raise MetadataApiError(response.text, response)

Expand Down
81 changes: 81 additions & 0 deletions cumulusci/salesforce_api/tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,3 +1013,84 @@ def test_process_response__no_package_match_found(self):
)
with pytest.raises(CumulusCIException):
api._process_response(response)


class TestCallExceptionPropagation:
"""Regression tests for SFDO-Tooling/CumulusCI#3939.

`BaseMetadataApiCall.__call__` historically wrapped every exception
from `_process_response` as `MetadataParseError`, masking native
CumulusCI exception types (`MetadataApiError`,
`MetadataComponentFailure`, `ApexTestException`) that
`_process_response` raises on legitimate failure cases.

Only genuinely unexpected exceptions (XML/parse errors, etc.) should
be wrapped; native exception types must propagate untouched so
callers can see the real error.
"""

def _make_api(self):
project_config = create_project_config("TestRepo", "TestOwner")
task = BaseTask(
project_config=project_config,
task_config=TaskConfig({}),
org_config=DummyOrgConfig({}),
)
api = BaseMetadataApiCall(task)
api._get_response = lambda: Response()
return api

def test_apex_test_exception_propagates_untouched(self):
api = self._make_api()
original = ApexTestException(
"Apex Test Failure: Class.MyTestClass.testIt: line 12, column 1"
)

def boom(response):
raise original

api._process_response = boom

with pytest.raises(ApexTestException) as excinfo:
api()
assert excinfo.value is original

def test_metadata_api_error_propagates_untouched(self):
api = self._make_api()
original = MetadataApiError("problem", Response())

def boom(response):
raise original

api._process_response = boom

with pytest.raises(MetadataApiError) as excinfo:
api()
assert excinfo.value is original
assert not isinstance(excinfo.value, MetadataParseError)

def test_metadata_component_failure_propagates_untouched(self):
api = self._make_api()
original = MetadataComponentFailure("component failed", Response())

def boom(response):
raise original

api._process_response = boom

with pytest.raises(MetadataComponentFailure) as excinfo:
api()
assert excinfo.value is original
assert not isinstance(excinfo.value, MetadataParseError)

def test_unexpected_exception_is_still_wrapped(self):
api = self._make_api()

def boom(response):
raise KeyError("unexpected_field")

api._process_response = boom

with pytest.raises(MetadataParseError) as excinfo:
api()
assert "unexpected_field" in str(excinfo.value)
Loading