Skip to content

Commit 07479b6

Browse files
committed
Add tests
1 parent 0bd6994 commit 07479b6

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

tests/functional/idempotency/_boto3/test_idempotency.py

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from aws_lambda_powertools.utilities.idempotency.serialization.dataclass import (
4747
DataclassSerializer,
4848
)
49+
from aws_lambda_powertools.utilities.typing import DurableContext
4950
from aws_lambda_powertools.utilities.validation import envelopes, validator
5051
from aws_lambda_powertools.warnings import PowertoolsUserWarning
5152
from tests.functional.idempotency.utils import (
@@ -2136,3 +2137,189 @@ def lambda_handler(record, context):
21362137
result = lambda_handler(mock_event, lambda_context)
21372138
# THEN we expect the function to execute successfully
21382139
assert result == expected_result
2140+
2141+
2142+
# Tests: Durable Functions Integration
2143+
2144+
2145+
@pytest.fixture
2146+
def durable_context_single_operation(lambda_context):
2147+
"""DurableContext with single operation (execution mode, is_replay=False)"""
2148+
durable_ctx = DurableContext()
2149+
durable_ctx._lambda_context = lambda_context
2150+
durable_ctx._state = Mock(operations=[{"id": "op1"}])
2151+
return durable_ctx
2152+
2153+
2154+
@pytest.fixture
2155+
def durable_context_multiple_operations(lambda_context):
2156+
"""DurableContext with multiple operations (replay mode, is_replay=True)"""
2157+
durable_ctx = DurableContext()
2158+
durable_ctx._lambda_context = lambda_context
2159+
durable_ctx._state = Mock(operations=[{"id": "op1"}, {"id": "op2"}])
2160+
return durable_ctx
2161+
2162+
2163+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
2164+
def test_idempotent_lambda_with_durable_context_first_execution(
2165+
idempotency_config: IdempotencyConfig,
2166+
persistence_store: DynamoDBPersistenceLayer,
2167+
lambda_apigw_event,
2168+
durable_context_single_operation,
2169+
lambda_response,
2170+
):
2171+
"""
2172+
Test idempotent decorator with DurableContext during first execution (execution mode).
2173+
2174+
When a durable function executes for the first time (single operation in state),
2175+
is_replay=False, and the function should execute normally, saving the result.
2176+
"""
2177+
# GIVEN
2178+
stubber = stub.Stubber(persistence_store.client)
2179+
stubber.add_response("put_item", {})
2180+
stubber.add_response("update_item", {})
2181+
stubber.activate()
2182+
2183+
# WHEN
2184+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
2185+
def lambda_handler(event, context):
2186+
return lambda_response
2187+
2188+
result = lambda_handler(lambda_apigw_event, durable_context_single_operation)
2189+
2190+
# THEN
2191+
assert result == lambda_response
2192+
stubber.assert_no_pending_responses()
2193+
stubber.deactivate()
2194+
2195+
2196+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
2197+
def test_idempotent_lambda_with_durable_context_during_replay(
2198+
idempotency_config: IdempotencyConfig,
2199+
persistence_store: DynamoDBPersistenceLayer,
2200+
lambda_apigw_event,
2201+
durable_context_multiple_operations,
2202+
timestamp_future,
2203+
lambda_response,
2204+
serialized_lambda_response,
2205+
):
2206+
"""
2207+
Test idempotent decorator with DurableContext during workflow replay (replay mode).
2208+
2209+
When a durable function replays (multiple operations in state), is_replay=True.
2210+
The function should execute once to get the response and save it, even when
2211+
an INPROGRESS record exists from a previous execution.
2212+
"""
2213+
# GIVEN
2214+
hashed_key = hash_idempotency_key(data=lambda_apigw_event)
2215+
2216+
stubber = stub.Stubber(persistence_store.client)
2217+
ddb_response = {
2218+
"Item": {
2219+
"id": {"S": hashed_key},
2220+
"expiration": {"N": timestamp_future},
2221+
"data": {"S": serialized_lambda_response},
2222+
"status": {"S": "INPROGRESS"},
2223+
},
2224+
}
2225+
stubber.add_client_error("put_item", "ConditionalCheckFailedException", modeled_fields=ddb_response)
2226+
# In replay mode, function still executes once to get response, then saves it
2227+
stubber.add_response("update_item", {})
2228+
stubber.activate()
2229+
2230+
# WHEN
2231+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
2232+
def lambda_handler(event, context):
2233+
return lambda_response
2234+
2235+
result = lambda_handler(lambda_apigw_event, durable_context_multiple_operations)
2236+
2237+
# THEN - Should return result in replay mode
2238+
assert result == lambda_response
2239+
stubber.assert_no_pending_responses()
2240+
stubber.deactivate()
2241+
2242+
2243+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
2244+
def test_idempotent_lambda_extracts_lambda_context_from_durable_context(
2245+
idempotency_config: IdempotencyConfig,
2246+
persistence_store: DynamoDBPersistenceLayer,
2247+
lambda_apigw_event,
2248+
durable_context_single_operation,
2249+
lambda_response,
2250+
):
2251+
"""
2252+
Test that idempotency properly extracts LambdaContext from DurableContext.
2253+
2254+
The @idempotent decorator should extract the wrapped lambda_context from
2255+
DurableContext for tracking remaining time and other Lambda-specific features.
2256+
"""
2257+
# GIVEN
2258+
stubber = stub.Stubber(persistence_store.client)
2259+
stubber.add_response("put_item", {})
2260+
stubber.add_response("update_item", {})
2261+
stubber.activate()
2262+
2263+
# WHEN
2264+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
2265+
def lambda_handler(event, context):
2266+
# Verify we can access lambda_context properties
2267+
assert hasattr(context, "lambda_context")
2268+
assert context.lambda_context.function_name == "test-func"
2269+
return lambda_response
2270+
2271+
result = lambda_handler(lambda_apigw_event, durable_context_single_operation)
2272+
2273+
# THEN
2274+
assert result == lambda_response
2275+
stubber.assert_no_pending_responses()
2276+
stubber.deactivate()
2277+
2278+
2279+
@pytest.mark.parametrize("idempotency_config", [{"use_local_cache": False}], indirect=True)
2280+
def test_idempotent_lambda_concurrent_durable_executions_raise_in_progress_error(
2281+
idempotency_config: IdempotencyConfig,
2282+
persistence_store: DynamoDBPersistenceLayer,
2283+
lambda_apigw_event,
2284+
durable_context_single_operation,
2285+
lambda_response,
2286+
):
2287+
"""
2288+
Test that concurrent durable executions are prevented by IdempotencyAlreadyInProgressError.
2289+
2290+
Scenario: Two different durable function executions attempt to process the same
2291+
idempotent operation concurrently:
2292+
1. First execution creates an INPROGRESS record
2293+
2. Second execution (in execution mode, is_replay=False) finds the INPROGRESS record
2294+
3. Second execution should raise IdempotencyAlreadyInProgressError to prevent duplicate work
2295+
2296+
This ensures data consistency when multiple durable function instances execute concurrently.
2297+
"""
2298+
# GIVEN
2299+
hashed_key = hash_idempotency_key(data=lambda_apigw_event)
2300+
2301+
stubber = stub.Stubber(persistence_store.client)
2302+
# Simulate existing INPROGRESS record with far future timestamps
2303+
ddb_response = {
2304+
"Item": {
2305+
"id": {"S": hashed_key},
2306+
"expiration": {"N": "9999999999"},
2307+
"in_progress_expiration": {"N": "9999999999999"}, # Far future in milliseconds
2308+
"status": {"S": "INPROGRESS"},
2309+
},
2310+
}
2311+
stubber.add_client_error("put_item", "ConditionalCheckFailedException", modeled_fields=ddb_response)
2312+
stubber.activate()
2313+
2314+
# WHEN / THEN - Should raise IdempotencyAlreadyInProgressError in execution mode
2315+
@idempotent(config=idempotency_config, persistence_store=persistence_store)
2316+
def lambda_handler(event, context):
2317+
return lambda_response
2318+
2319+
with pytest.raises(IdempotencyAlreadyInProgressError) as exc_info:
2320+
lambda_handler(lambda_apigw_event, durable_context_single_operation)
2321+
2322+
# Verify error message contains the idempotency key
2323+
assert hashed_key in str(exc_info.value)
2324+
stubber.assert_no_pending_responses()
2325+
stubber.deactivate()

0 commit comments

Comments
 (0)