8
8
from botocore import stub
9
9
from botocore .config import Config
10
10
from pydantic import BaseModel
11
+ from pytest import FixtureRequest
11
12
12
13
from aws_lambda_powertools .utilities .data_classes import (
13
14
APIGatewayProxyEventV2 ,
38
39
BasePersistenceLayer ,
39
40
DataRecord ,
40
41
)
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
+ )
44
51
from aws_lambda_powertools .utilities .validation import envelopes , validator
45
52
from tests .functional .idempotency .utils import (
53
+ build_idempotency_put_item_response_stub ,
46
54
build_idempotency_put_item_stub ,
47
55
build_idempotency_update_item_stub ,
48
56
hash_idempotency_key ,
@@ -406,6 +414,8 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload(
406
414
Test idempotent decorator where event with matching event key has already been successfully processed
407
415
"""
408
416
417
+ # GIVEN an idempotent record already exists for the same transaction
418
+ # and payload validation was enabled ('validation' key)
409
419
stubber = stub .Stubber (persistence_store .client )
410
420
ddb_response = {
411
421
"Item" : {
@@ -423,8 +433,11 @@ def test_idempotent_lambda_already_completed_with_validation_bad_payload(
423
433
def lambda_handler (event , context ):
424
434
return lambda_response
425
435
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
426
440
with pytest .raises (IdempotencyValidationError ):
427
- lambda_apigw_event ["requestContext" ]["accountId" ] += "1" # Alter the request payload
428
441
lambda_handler (lambda_apigw_event , lambda_context )
429
442
430
443
stubber .assert_no_pending_responses ()
@@ -1172,11 +1185,9 @@ def _put_record(self, data_record: DataRecord) -> None:
1172
1185
def _update_record (self , data_record : DataRecord ) -> None :
1173
1186
assert data_record .idempotency_key == self .expected_idempotency_key
1174
1187
1175
- def _get_record (self , idempotency_key ) -> DataRecord :
1176
- ...
1188
+ def _get_record (self , idempotency_key ) -> DataRecord : ...
1177
1189
1178
- def _delete_record (self , data_record : DataRecord ) -> None :
1179
- ...
1190
+ def _delete_record (self , data_record : DataRecord ) -> None : ...
1180
1191
1181
1192
1182
1193
def test_idempotent_lambda_event_source (lambda_context ):
@@ -1860,3 +1871,60 @@ def lambda_handler(event, context):
1860
1871
1861
1872
stubber .assert_no_pending_responses ()
1862
1873
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