Skip to content

Commit 9e0e3a3

Browse files
fix(testing): reject blank execution ARNs
* fix(testing): reject blank execution ARNs * amend err msg and validate method name closes #636 Co-authored-by: thomas <18520168+yaythomas@users.noreply.github.com> --------- Co-authored-by: thomas <18520168+yaythomas@users.noreply.github.com>
1 parent caa57ad commit 9e0e3a3

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/aws-durable-execution-sdk-python-testing/src/aws_durable_execution_sdk_python_testing/executor.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,22 @@ def timeout_handler():
196196
execution_arn=execution.durable_execution_arn
197197
)
198198

199+
@staticmethod
200+
def _validate_execution_arn(execution_arn: str) -> None:
201+
"""Reject a blank or non-string execution ARN before it reaches the registry or store.
202+
203+
This runner's execution ARNs are opaque local identifiers, not
204+
real AWS ARNs (see ``Execution.new``), so this only rules out
205+
empty/malformed input rather than checking AWS ARN shape.
206+
207+
Raises:
208+
InvalidParameterValueException: If the ARN is not a
209+
non-empty string.
210+
"""
211+
if not isinstance(execution_arn, str) or not execution_arn.strip():
212+
msg: str = "Invalid Durable Execution ARN"
213+
raise InvalidParameterValueException(msg)
214+
199215
def get_execution(self, execution_arn: str) -> Execution:
200216
"""Get execution by ARN.
201217
@@ -206,8 +222,10 @@ def get_execution(self, execution_arn: str) -> Execution:
206222
Execution: The execution object
207223
208224
Raises:
225+
InvalidParameterValueException: If the ARN is blank.
209226
ResourceNotFoundException: If execution does not exist
210227
"""
228+
self._validate_execution_arn(execution_arn)
211229
try:
212230
return self._store.load(execution_arn)
213231
except KeyError as e:
@@ -376,8 +394,10 @@ def stop_execution(
376394
StopDurableExecutionResponse: Response containing end timestamp
377395
378396
Raises:
397+
InvalidParameterValueException: If the ARN is blank.
379398
ResourceNotFoundException: If execution does not exist
380399
"""
400+
self._validate_execution_arn(execution_arn)
381401
return self._registry.submit(
382402
execution_arn,
383403
CallableTask(lambda: self._apply_stop(execution_arn, error)),
@@ -415,7 +435,12 @@ def get_execution_state(
415435
marker: str | None = None,
416436
max_items: int | None = None,
417437
) -> GetDurableExecutionStateResponse:
418-
"""Return a page of operations, serialized on the execution's worker."""
438+
"""Return a page of operations, serialized on the execution's worker.
439+
440+
Raises:
441+
InvalidParameterValueException: If the ARN is blank.
442+
"""
443+
self._validate_execution_arn(execution_arn)
419444
return self._registry.submit(
420445
execution_arn,
421446
CallableTask(
@@ -795,7 +820,11 @@ def checkpoint_execution(
795820
796821
Routes through the per-execution worker so checkpoints for one
797822
execution never overlap.
823+
824+
Raises:
825+
InvalidParameterValueException: If the ARN is blank.
798826
"""
827+
self._validate_execution_arn(execution_arn)
799828
return self._registry.submit(
800829
execution_arn,
801830
CallableTask(

packages/aws-durable-execution-sdk-python-testing/tests/executor_test.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,44 @@ def test_get_execution_not_found(executor, mock_store):
18461846
executor.get_execution("test-arn")
18471847

18481848

1849+
@pytest.mark.parametrize("blank_arn", ["", " ", None])
1850+
def test_get_execution_rejects_blank_arn(executor, mock_store, blank_arn):
1851+
with pytest.raises(InvalidParameterValueException):
1852+
executor.get_execution(blank_arn)
1853+
1854+
mock_store.load.assert_not_called()
1855+
1856+
1857+
@pytest.mark.parametrize("blank_arn", ["", " ", None])
1858+
def test_stop_execution_rejects_blank_arn(executor, mock_store, blank_arn):
1859+
"""A blank ARN must be rejected before it reaches the registry, so it
1860+
never creates a permanent phantom worker for an execution that was
1861+
never going to exist."""
1862+
with pytest.raises(InvalidParameterValueException):
1863+
executor.stop_execution(blank_arn)
1864+
1865+
mock_store.load.assert_not_called()
1866+
assert executor._registry.active_count() == 0 # noqa: SLF001
1867+
1868+
1869+
@pytest.mark.parametrize("blank_arn", ["", " ", None])
1870+
def test_get_execution_state_rejects_blank_arn(executor, mock_store, blank_arn):
1871+
with pytest.raises(InvalidParameterValueException):
1872+
executor.get_execution_state(blank_arn, checkpoint_token="token")
1873+
1874+
mock_store.load.assert_not_called()
1875+
assert executor._registry.active_count() == 0 # noqa: SLF001
1876+
1877+
1878+
@pytest.mark.parametrize("blank_arn", ["", " ", None])
1879+
def test_checkpoint_execution_rejects_blank_arn(executor, mock_store, blank_arn):
1880+
with pytest.raises(InvalidParameterValueException):
1881+
executor.checkpoint_execution(blank_arn, checkpoint_token="token")
1882+
1883+
mock_store.load.assert_not_called()
1884+
assert executor._registry.active_count() == 0 # noqa: SLF001
1885+
1886+
18491887
def test_get_execution_state(mock_scheduler, mock_invoker, mock_checkpoint_processor):
18501888
"""GetDurableExecutionState is a pure read from the pinned
18511889
snapshot, bounded by the configured byte cap. Uses a real store

0 commit comments

Comments
 (0)