|
1 | 1 | import copy
|
| 2 | +import datetime |
2 | 3 | import sys
|
| 4 | +import warnings |
3 | 5 | from hashlib import md5
|
4 | 6 | from unittest.mock import MagicMock
|
5 | 7 |
|
|
10 | 12 |
|
11 | 13 | from aws_lambda_powertools.utilities.data_classes import APIGatewayProxyEventV2, event_source
|
12 | 14 | from aws_lambda_powertools.utilities.idempotency import DynamoDBPersistenceLayer, IdempotencyConfig
|
13 |
| -from aws_lambda_powertools.utilities.idempotency.base import _prepare_data |
| 15 | +from aws_lambda_powertools.utilities.idempotency.base import MAX_RETRIES, _prepare_data |
14 | 16 | from aws_lambda_powertools.utilities.idempotency.exceptions import (
|
15 | 17 | IdempotencyAlreadyInProgressError,
|
16 | 18 | IdempotencyInconsistentStateError,
|
@@ -386,7 +388,7 @@ def test_idempotent_lambda_expired(
|
386 | 388 | lambda_context,
|
387 | 389 | ):
|
388 | 390 | """
|
389 |
| - Test idempotent decorator when lambda is called with an event it succesfully handled already, but outside of the |
| 391 | + Test idempotent decorator when lambda is called with an event it successfully handled already, but outside of the |
390 | 392 | expiry window
|
391 | 393 | """
|
392 | 394 |
|
@@ -529,7 +531,7 @@ def test_idempotent_lambda_expired_during_request(
|
529 | 531 | lambda_context,
|
530 | 532 | ):
|
531 | 533 | """
|
532 |
| - Test idempotent decorator when lambda is called with an event it succesfully handled already. Persistence store |
| 534 | + Test idempotent decorator when lambda is called with an event it successfully handled already. Persistence store |
533 | 535 | returns inconsistent/rapidly changing result between put_item and get_item calls.
|
534 | 536 | """
|
535 | 537 |
|
@@ -804,6 +806,156 @@ def lambda_handler(event, context):
|
804 | 806 | stubber.deactivate()
|
805 | 807 |
|
806 | 808 |
|
| 809 | +@pytest.mark.parametrize( |
| 810 | + "idempotency_config", |
| 811 | + [ |
| 812 | + {"use_local_cache": False, "expires_in_progress": True}, |
| 813 | + {"use_local_cache": True, "expires_in_progress": True}, |
| 814 | + ], |
| 815 | + indirect=True, |
| 816 | +) |
| 817 | +def test_idempotent_lambda_expires_in_progress_before_expire( |
| 818 | + idempotency_config: IdempotencyConfig, |
| 819 | + persistence_store: DynamoDBPersistenceLayer, |
| 820 | + lambda_apigw_event, |
| 821 | + timestamp_future, |
| 822 | + lambda_response, |
| 823 | + hashed_idempotency_key, |
| 824 | + lambda_context, |
| 825 | +): |
| 826 | + """ |
| 827 | + Test idempotent decorator when expires_in_progress is on and the event is still in progress, before the |
| 828 | + lambda expiration window. |
| 829 | + """ |
| 830 | + |
| 831 | + stubber = stub.Stubber(persistence_store.table.meta.client) |
| 832 | + |
| 833 | + stubber.add_client_error("put_item", "ConditionalCheckFailedException") |
| 834 | + |
| 835 | + now = datetime.datetime.now() |
| 836 | + period = datetime.timedelta(seconds=5) |
| 837 | + timestamp_expires_in_progress = str(int((now + period).timestamp())) |
| 838 | + |
| 839 | + expected_params_get_item = { |
| 840 | + "TableName": TABLE_NAME, |
| 841 | + "Key": {"id": hashed_idempotency_key}, |
| 842 | + "ConsistentRead": True, |
| 843 | + } |
| 844 | + ddb_response_get_item = { |
| 845 | + "Item": { |
| 846 | + "id": {"S": hashed_idempotency_key}, |
| 847 | + "expiration": {"N": timestamp_future}, |
| 848 | + "in_progress_expiration": {"N": timestamp_expires_in_progress}, |
| 849 | + "data": {"S": '{"message": "test", "statusCode": 200'}, |
| 850 | + "status": {"S": "INPROGRESS"}, |
| 851 | + } |
| 852 | + } |
| 853 | + stubber.add_response("get_item", ddb_response_get_item, expected_params_get_item) |
| 854 | + |
| 855 | + stubber.activate() |
| 856 | + |
| 857 | + @idempotent(config=idempotency_config, persistence_store=persistence_store) |
| 858 | + def lambda_handler(event, context): |
| 859 | + return lambda_response |
| 860 | + |
| 861 | + with pytest.raises(IdempotencyAlreadyInProgressError): |
| 862 | + lambda_handler(lambda_apigw_event, lambda_context) |
| 863 | + |
| 864 | + stubber.assert_no_pending_responses() |
| 865 | + stubber.deactivate() |
| 866 | + |
| 867 | + |
| 868 | +@pytest.mark.parametrize( |
| 869 | + "idempotency_config", |
| 870 | + [ |
| 871 | + {"use_local_cache": False, "expires_in_progress": True}, |
| 872 | + {"use_local_cache": True, "expires_in_progress": True}, |
| 873 | + ], |
| 874 | + indirect=True, |
| 875 | +) |
| 876 | +def test_idempotent_lambda_expires_in_progress_after_expire( |
| 877 | + idempotency_config: IdempotencyConfig, |
| 878 | + persistence_store: DynamoDBPersistenceLayer, |
| 879 | + lambda_apigw_event, |
| 880 | + timestamp_future, |
| 881 | + lambda_response, |
| 882 | + hashed_idempotency_key, |
| 883 | + lambda_context, |
| 884 | +): |
| 885 | + stubber = stub.Stubber(persistence_store.table.meta.client) |
| 886 | + |
| 887 | + for _ in range(MAX_RETRIES + 1): |
| 888 | + stubber.add_client_error("put_item", "ConditionalCheckFailedException") |
| 889 | + |
| 890 | + one_second_ago = datetime.datetime.now() - datetime.timedelta(seconds=1) |
| 891 | + expected_params_get_item = { |
| 892 | + "TableName": TABLE_NAME, |
| 893 | + "Key": {"id": hashed_idempotency_key}, |
| 894 | + "ConsistentRead": True, |
| 895 | + } |
| 896 | + ddb_response_get_item = { |
| 897 | + "Item": { |
| 898 | + "id": {"S": hashed_idempotency_key}, |
| 899 | + "expiration": {"N": timestamp_future}, |
| 900 | + "in_progress_expiration": {"N": str(int(one_second_ago.timestamp()))}, |
| 901 | + "data": {"S": '{"message": "test", "statusCode": 200'}, |
| 902 | + "status": {"S": "INPROGRESS"}, |
| 903 | + } |
| 904 | + } |
| 905 | + stubber.add_response("get_item", ddb_response_get_item, expected_params_get_item) |
| 906 | + |
| 907 | + stubber.activate() |
| 908 | + |
| 909 | + @idempotent(config=idempotency_config, persistence_store=persistence_store) |
| 910 | + def lambda_handler(event, context): |
| 911 | + return lambda_response |
| 912 | + |
| 913 | + with pytest.raises(IdempotencyInconsistentStateError): |
| 914 | + lambda_handler(lambda_apigw_event, lambda_context) |
| 915 | + |
| 916 | + stubber.assert_no_pending_responses() |
| 917 | + stubber.deactivate() |
| 918 | + |
| 919 | + |
| 920 | +@pytest.mark.parametrize( |
| 921 | + "idempotency_config", |
| 922 | + [ |
| 923 | + {"use_local_cache": False, "expires_in_progress": True}, |
| 924 | + {"use_local_cache": True, "expires_in_progress": True}, |
| 925 | + ], |
| 926 | + indirect=True, |
| 927 | +) |
| 928 | +def test_idempotent_lambda_expires_in_progress_unavailable_remaining_time( |
| 929 | + idempotency_config: IdempotencyConfig, |
| 930 | + persistence_store: DynamoDBPersistenceLayer, |
| 931 | + lambda_apigw_event, |
| 932 | + lambda_response, |
| 933 | + lambda_context, |
| 934 | + expected_params_put_item, |
| 935 | + expected_params_update_item, |
| 936 | +): |
| 937 | + stubber = stub.Stubber(persistence_store.table.meta.client) |
| 938 | + |
| 939 | + ddb_response = {} |
| 940 | + stubber.add_response("put_item", ddb_response, expected_params_put_item) |
| 941 | + stubber.add_response("update_item", ddb_response, expected_params_update_item) |
| 942 | + |
| 943 | + stubber.activate() |
| 944 | + |
| 945 | + @idempotent(config=idempotency_config, persistence_store=persistence_store) |
| 946 | + def lambda_handler(event, context): |
| 947 | + return lambda_response |
| 948 | + |
| 949 | + with warnings.catch_warnings(record=True) as w: |
| 950 | + warnings.simplefilter("default") |
| 951 | + lambda_handler(lambda_apigw_event, lambda_context) |
| 952 | + assert len(w) == 1 |
| 953 | + assert str(w[-1].message) == "Expires in progress is enabled but we couldn't determine the remaining time left" |
| 954 | + |
| 955 | + stubber.assert_no_pending_responses() |
| 956 | + stubber.deactivate() |
| 957 | + |
| 958 | + |
807 | 959 | def test_data_record_invalid_status_value():
|
808 | 960 | data_record = DataRecord("key", status="UNSUPPORTED_STATUS")
|
809 | 961 | with pytest.raises(IdempotencyInvalidStatusError) as e:
|
|
0 commit comments