88from botocore import stub
99from botocore .config import Config
1010from pydantic import BaseModel
11+ from pytest import FixtureRequest
1112
1213from aws_lambda_powertools .utilities .data_classes import (
1314 APIGatewayProxyEventV2 ,
3839 BasePersistenceLayer ,
3940 DataRecord ,
4041)
41- from aws_lambda_powertools .utilities .idempotency .serialization .custom_dict import CustomDictSerializer
42- from aws_lambda_powertools .utilities .idempotency .serialization .dataclass import DataclassSerializer
43- from aws_lambda_powertools .utilities .idempotency .serialization .pydantic import PydanticSerializer
42+ from aws_lambda_powertools .utilities .idempotency .serialization .custom_dict import (
43+ CustomDictSerializer ,
44+ )
45+ from aws_lambda_powertools .utilities .idempotency .serialization .dataclass import (
46+ DataclassSerializer ,
47+ )
48+ from aws_lambda_powertools .utilities .idempotency .serialization .pydantic import (
49+ PydanticSerializer ,
50+ )
4451from aws_lambda_powertools .utilities .validation import envelopes , validator
4552from tests .functional .idempotency .utils import (
53+ build_idempotency_put_item_response_stub ,
4654 build_idempotency_put_item_stub ,
4755 build_idempotency_update_item_stub ,
4856 hash_idempotency_key ,
@@ -406,6 +414,8 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload(
406414 Test idempotent decorator where event with matching event key has already been successfully processed
407415 """
408416
417+ # GIVEN an idempotent record already exists for the same transaction
418+ # and payload validation was enabled ('validation' key)
409419 stubber = stub .Stubber (persistence_store .client )
410420 ddb_response = {
411421 "Item" : {
@@ -423,8 +433,11 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload(
423433 def lambda_handler (event , context ):
424434 return lambda_response
425435
436+ # WHEN the subsequent request is the same but validated field is tampered
437+ lambda_apigw_event ["requestContext" ]["accountId" ] += "1" # Alter the request payload
438+
439+ # THEN we should raise
426440 with pytest .raises (IdempotencyValidationError ):
427- lambda_apigw_event ["requestContext" ]["accountId" ] += "1" # Alter the request payload
428441 lambda_handler (lambda_apigw_event , lambda_context )
429442
430443 stubber .assert_no_pending_responses ()
@@ -1172,11 +1185,9 @@ def _put_record(self, data_record: DataRecord) -> None:
11721185 def _update_record (self , data_record : DataRecord ) -> None :
11731186 assert data_record .idempotency_key == self .expected_idempotency_key
11741187
1175- def _get_record (self , idempotency_key ) -> DataRecord :
1176- ...
1188+ def _get_record (self , idempotency_key ) -> DataRecord : ...
11771189
1178- def _delete_record (self , data_record : DataRecord ) -> None :
1179- ...
1190+ def _delete_record (self , data_record : DataRecord ) -> None : ...
11801191
11811192
11821193def test_idempotent_lambda_event_source (lambda_context ):
@@ -1860,3 +1871,60 @@ def lambda_handler(event, context):
18601871
18611872 stubber .assert_no_pending_responses ()
18621873 stubber .deactivate ()
1874+
1875+
1876+ def test_idempotency_payload_validation_with_tampering_nested_object (
1877+ persistence_store : DynamoDBPersistenceLayer ,
1878+ timestamp_future ,
1879+ lambda_context ,
1880+ request : FixtureRequest ,
1881+ ):
1882+ # GIVEN an idempotency config with a compound idempotency key (refund, customer_id)
1883+ # AND with payload validation key to prevent tampering
1884+
1885+ validation_key = "details"
1886+ idempotency_config = IdempotencyConfig (
1887+ event_key_jmespath = '["refund_id", "customer_id"]' ,
1888+ payload_validation_jmespath = validation_key ,
1889+ use_local_cache = False ,
1890+ )
1891+
1892+ # AND a previous transaction already processed in the persistent store
1893+ transaction = {
1894+ "refund_id" : "ffd11882-d476-4598-bbf1-643f2be5addf" ,
1895+ "customer_id" : "9e9fc440-9e65-49b5-9e71-1382ea1b1658" ,
1896+ "details" : [
1897+ {
1898+ "company_name" : "Parker, Johnson and Rath" ,
1899+ "currency" : "Turkish Lira" ,
1900+ },
1901+ ],
1902+ }
1903+
1904+ stubber = stub .Stubber (persistence_store .client )
1905+ ddb_response = build_idempotency_put_item_response_stub (
1906+ data = transaction ,
1907+ expiration = timestamp_future ,
1908+ status = "COMPLETED" ,
1909+ request = request ,
1910+ validation_data = transaction [validation_key ],
1911+ )
1912+
1913+ stubber .add_client_error ("put_item" , "ConditionalCheckFailedException" , modeled_fields = ddb_response )
1914+ stubber .activate ()
1915+
1916+ # AND an upcoming tampered transaction
1917+ tampered_transaction = copy .deepcopy (transaction )
1918+ tampered_transaction ["details" ][0 ]["currency" ] = "Euro"
1919+
1920+ @idempotent (config = idempotency_config , persistence_store = persistence_store )
1921+ def lambda_handler (event , context ):
1922+ return event
1923+
1924+ # WHEN the tampered request is made
1925+ # THEN we should raise
1926+ with pytest .raises (IdempotencyValidationError ):
1927+ lambda_handler (tampered_transaction , lambda_context )
1928+
1929+ stubber .assert_no_pending_responses ()
1930+ stubber .deactivate ()
0 commit comments