1414
1515package software .amazon .lambda .powertools .idempotency .internal ;
1616
17- import com .amazonaws .services .lambda .runtime .Context ;
18- import com .fasterxml .jackson .core .JsonProcessingException ;
19- import com .fasterxml .jackson .databind .JsonNode ;
17+ import static java .time .temporal .ChronoUnit .SECONDS ;
18+ import static org .assertj .core .api .Assertions .assertThat ;
19+ import static org .assertj .core .api .Assertions .assertThatThrownBy ;
20+ import static org .mockito .ArgumentMatchers .any ;
21+ import static org .mockito .Mockito .doReturn ;
22+ import static org .mockito .Mockito .doThrow ;
23+ import static org .mockito .Mockito .spy ;
24+ import static org .mockito .Mockito .verify ;
25+ import static org .mockito .Mockito .verifyNoInteractions ;
26+ import static org .mockito .Mockito .when ;
27+
28+ import java .time .Instant ;
29+ import java .util .OptionalInt ;
30+ import java .util .OptionalLong ;
31+
2032import org .junit .jupiter .api .BeforeEach ;
2133import org .junit .jupiter .api .Test ;
2234import org .junitpioneer .jupiter .SetEnvironmentVariable ;
2335import org .mockito .ArgumentCaptor ;
2436import org .mockito .Mock ;
2537import org .mockito .MockitoAnnotations ;
38+
39+ import com .amazonaws .services .lambda .runtime .Context ;
40+ import com .fasterxml .jackson .core .JsonProcessingException ;
41+ import com .fasterxml .jackson .databind .JsonNode ;
42+
2643import software .amazon .lambda .powertools .idempotency .Constants ;
2744import software .amazon .lambda .powertools .idempotency .Idempotency ;
2845import software .amazon .lambda .powertools .idempotency .IdempotencyConfig ;
4360import software .amazon .lambda .powertools .idempotency .persistence .DataRecord ;
4461import software .amazon .lambda .powertools .utilities .JsonConfig ;
4562
46- import java .time .Instant ;
47- import java .util .OptionalInt ;
48- import java .util .OptionalLong ;
49-
50- import static java .time .temporal .ChronoUnit .SECONDS ;
51- import static org .assertj .core .api .Assertions .assertThat ;
52- import static org .assertj .core .api .Assertions .assertThatThrownBy ;
53- import static org .mockito .ArgumentMatchers .any ;
54- import static org .mockito .Mockito .doReturn ;
55- import static org .mockito .Mockito .doThrow ;
56- import static org .mockito .Mockito .spy ;
57- import static org .mockito .Mockito .verify ;
58- import static org .mockito .Mockito .verifyNoInteractions ;
59- import static org .mockito .Mockito .when ;
60-
6163public class IdempotencyAspectTest {
6264
6365 @ Mock
@@ -77,8 +79,8 @@ public void firstCall_shouldPutInStore() {
7779 .withPersistenceStore (store )
7880 .withConfig (IdempotencyConfig .builder ()
7981 .withEventKeyJMESPath ("id" )
80- .build ()
81- ) .configure ();
82+ .build ())
83+ .configure ();
8284
8385 IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
8486
@@ -103,27 +105,66 @@ public void firstCall_shouldPutInStore() {
103105 assertThat (resultCaptor .getValue ()).isEqualTo (basket );
104106 }
105107
108+ @ Test
109+ public void firstCall_shouldPutInStoreAndNotApplyResponseHook () {
110+ Idempotency .config ()
111+ .withPersistenceStore (store )
112+ .withConfig (IdempotencyConfig .builder ()
113+ .withEventKeyJMESPath ("id" )
114+ // This hook will add a second product to the basket. It should not run here. Only for
115+ // idempotent responses.
116+ .withResponseHook ((responseData , dataRecord ) -> {
117+ final Basket basket = (Basket ) responseData ;
118+ basket .add (new Product (42 , "fake product 2" , 12 ));
119+ return basket ;
120+ })
121+ .build ())
122+ .configure ();
123+
124+ IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
125+
126+ when (context .getRemainingTimeInMillis ()).thenReturn (30000 );
127+
128+ Product p = new Product (42 , "fake product" , 12 );
129+ Basket basket = function .handleRequest (p , context );
130+ assertThat (basket .getProducts ()).hasSize (1 ); // Size should be 1 because response hook should not run
131+ assertThat (function .handlerCalled ()).isTrue ();
132+
133+ ArgumentCaptor <JsonNode > nodeCaptor = ArgumentCaptor .forClass (JsonNode .class );
134+ ArgumentCaptor <OptionalInt > expiryCaptor = ArgumentCaptor .forClass (OptionalInt .class );
135+ verify (store ).saveInProgress (nodeCaptor .capture (), any (), expiryCaptor .capture ());
136+ assertThat (nodeCaptor .getValue ().get ("id" ).asLong ()).isEqualTo (p .getId ());
137+ assertThat (nodeCaptor .getValue ().get ("name" ).asText ()).isEqualTo (p .getName ());
138+ assertThat (nodeCaptor .getValue ().get ("price" ).asDouble ()).isEqualTo (p .getPrice ());
139+
140+ assertThat (expiryCaptor .getValue ().orElse (-1 )).isEqualTo (30000 );
141+
142+ ArgumentCaptor <Basket > resultCaptor = ArgumentCaptor .forClass (Basket .class );
143+ verify (store ).saveSuccess (any (), resultCaptor .capture (), any ());
144+ assertThat (resultCaptor .getValue ()).isEqualTo (basket );
145+ }
146+
106147 @ Test
107148 public void secondCall_notExpired_shouldGetFromStore () throws JsonProcessingException {
108149 // GIVEN
109150 Idempotency .config ()
110151 .withPersistenceStore (store )
111152 .withConfig (IdempotencyConfig .builder ()
112153 .withEventKeyJMESPath ("id" )
113- .build ()
114- ) .configure ();
154+ .build ())
155+ .configure ();
115156
116157 doThrow (IdempotencyItemAlreadyExistsException .class ).when (store ).saveInProgress (any (), any (), any ());
117158
118159 Product p = new Product (42 , "fake product" , 12 );
119160 Basket b = new Basket (p );
120- DataRecord record = new DataRecord (
161+ DataRecord dr = new DataRecord (
121162 "42" ,
122163 DataRecord .Status .COMPLETED ,
123164 Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
124165 JsonConfig .get ().getObjectMapper ().writer ().writeValueAsString (b ),
125166 null );
126- doReturn (record ).when (store ).getRecord (any (), any ());
167+ doReturn (dr ).when (store ).getRecord (any (), any ());
127168
128169 // WHEN
129170 IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
@@ -141,19 +182,19 @@ public void secondCall_notExpired_shouldGetStringFromStore() {
141182 .withPersistenceStore (store )
142183 .withConfig (IdempotencyConfig .builder ()
143184 .withEventKeyJMESPath ("id" )
144- .build ()
145- ) .configure ();
185+ .build ())
186+ .configure ();
146187
147188 doThrow (IdempotencyItemAlreadyExistsException .class ).when (store ).saveInProgress (any (), any (), any ());
148189
149190 Product p = new Product (42 , "fake product" , 12 );
150- DataRecord record = new DataRecord (
191+ DataRecord dr = new DataRecord (
151192 "42" ,
152193 DataRecord .Status .COMPLETED ,
153194 Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
154195 p .getName (),
155196 null );
156- doReturn (record ).when (store ).getRecord (any (), any ());
197+ doReturn (dr ).when (store ).getRecord (any (), any ());
157198
158199 // WHEN
159200 IdempotencyStringFunction function = new IdempotencyStringFunction ();
@@ -164,6 +205,41 @@ public void secondCall_notExpired_shouldGetStringFromStore() {
164205 assertThat (function .handlerCalled ()).isFalse ();
165206 }
166207
208+ @ Test
209+ public void secondCall_notExpired_shouldGetStringFromStoreWithResponseHook () {
210+ // GIVEN
211+ final String RESPONSE_HOOK_SUFFIX = " ResponseHook" ;
212+ Idempotency .config ()
213+ .withPersistenceStore (store )
214+ .withConfig (IdempotencyConfig .builder ()
215+ .withEventKeyJMESPath ("id" )
216+ .withResponseHook ((responseData , dataRecord ) -> {
217+ responseData += RESPONSE_HOOK_SUFFIX ;
218+ return responseData ;
219+ })
220+ .build ())
221+ .configure ();
222+
223+ doThrow (IdempotencyItemAlreadyExistsException .class ).when (store ).saveInProgress (any (), any (), any ());
224+
225+ Product p = new Product (42 , "fake product" , 12 );
226+ DataRecord dr = new DataRecord (
227+ "42" ,
228+ DataRecord .Status .COMPLETED ,
229+ Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
230+ p .getName (),
231+ null );
232+ doReturn (dr ).when (store ).getRecord (any (), any ());
233+
234+ // WHEN
235+ IdempotencyStringFunction function = new IdempotencyStringFunction ();
236+ String name = function .handleRequest (p , context );
237+
238+ // THEN
239+ assertThat (name ).isEqualTo (p .getName () + RESPONSE_HOOK_SUFFIX );
240+ assertThat (function .handlerCalled ()).isFalse ();
241+ }
242+
167243 @ Test
168244 public void secondCall_inProgress_shouldThrowIdempotencyAlreadyInProgressException ()
169245 throws JsonProcessingException {
@@ -172,23 +248,23 @@ public void secondCall_inProgress_shouldThrowIdempotencyAlreadyInProgressExcepti
172248 .withPersistenceStore (store )
173249 .withConfig (IdempotencyConfig .builder ()
174250 .withEventKeyJMESPath ("id" )
175- .build ()
176- ) .configure ();
251+ .build ())
252+ .configure ();
177253
178254 doThrow (IdempotencyItemAlreadyExistsException .class ).when (store ).saveInProgress (any (), any (), any ());
179255
180256 Product p = new Product (42 , "fake product" , 12 );
181257 Basket b = new Basket (p );
182- OptionalLong timestampInFuture =
183- OptionalLong . of ( Instant . now (). toEpochMilli () + 1000 ); // timeout not expired (in 1sec)
184- DataRecord record = new DataRecord (
258+ OptionalLong timestampInFuture = OptionalLong . of ( Instant . now (). toEpochMilli () + 1000 ); // timeout not expired
259+ // (in 1sec)
260+ DataRecord dr = new DataRecord (
185261 "42" ,
186262 DataRecord .Status .INPROGRESS ,
187263 Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
188264 JsonConfig .get ().getObjectMapper ().writer ().writeValueAsString (b ),
189265 null ,
190266 timestampInFuture );
191- doReturn (record ).when (store ).getRecord (any (), any ());
267+ doReturn (dr ).when (store ).getRecord (any (), any ());
192268
193269 // THEN
194270 IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
@@ -204,23 +280,23 @@ public void secondCall_inProgress_lambdaTimeout_timeoutExpired_shouldThrowIncons
204280 .withPersistenceStore (store )
205281 .withConfig (IdempotencyConfig .builder ()
206282 .withEventKeyJMESPath ("id" )
207- .build ()
208- ) .configure ();
283+ .build ())
284+ .configure ();
209285
210286 doThrow (IdempotencyItemAlreadyExistsException .class ).when (store ).saveInProgress (any (), any (), any ());
211287
212288 Product p = new Product (42 , "fake product" , 12 );
213289 Basket b = new Basket (p );
214- OptionalLong timestampInThePast =
215- OptionalLong . of ( Instant . now (). toEpochMilli () - 100 ); // timeout expired 100ms ago
216- DataRecord record = new DataRecord (
290+ OptionalLong timestampInThePast = OptionalLong . of ( Instant . now (). toEpochMilli () - 100 ); // timeout expired 100ms
291+ // ago
292+ DataRecord dr = new DataRecord (
217293 "42" ,
218294 DataRecord .Status .INPROGRESS ,
219295 Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
220296 JsonConfig .get ().getObjectMapper ().writer ().writeValueAsString (b ),
221297 null ,
222298 timestampInThePast );
223- doReturn (record ).when (store ).getRecord (any (), any ());
299+ doReturn (dr ).when (store ).getRecord (any (), any ());
224300
225301 // THEN
226302 IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
@@ -235,8 +311,8 @@ public void functionThrowException_shouldDeleteRecord_andThrowFunctionException(
235311 .withPersistenceStore (store )
236312 .withConfig (IdempotencyConfig .builder ()
237313 .withEventKeyJMESPath ("id" )
238- .build ()
239- ) .configure ();
314+ .build ())
315+ .configure ();
240316
241317 // WHEN / THEN
242318 IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction ();
@@ -256,8 +332,8 @@ public void testIdempotencyDisabled_shouldJustRunTheFunction() {
256332 .withPersistenceStore (store )
257333 .withConfig (IdempotencyConfig .builder ()
258334 .withEventKeyJMESPath ("id" )
259- .build ()
260- ) .configure ();
335+ .build ())
336+ .configure ();
261337
262338 // WHEN
263339 IdempotencyEnabledFunction function = new IdempotencyEnabledFunction ();
@@ -340,13 +416,13 @@ public void idempotencyOnSubMethodAnnotated_secondCall_notExpired_shouldGetFromS
340416
341417 Product p = new Product (42 , "fake product" , 12 );
342418 Basket b = new Basket (p );
343- DataRecord record = new DataRecord (
419+ DataRecord dr = new DataRecord (
344420 "fake" ,
345421 DataRecord .Status .COMPLETED ,
346422 Instant .now ().plus (356 , SECONDS ).getEpochSecond (),
347423 JsonConfig .get ().getObjectMapper ().writer ().writeValueAsString (b ),
348424 null );
349- doReturn (record ).when (store ).getRecord (any (), any ());
425+ doReturn (dr ).when (store ).getRecord (any (), any ());
350426
351427 // WHEN
352428 IdempotencyInternalFunction function = new IdempotencyInternalFunction (false );
@@ -405,8 +481,7 @@ public void idempotencyOnSubMethodAnnotated_keyJMESPathArray_shouldPutInStoreWit
405481 public void idempotencyOnSubMethodNotAnnotated_shouldThrowException () {
406482 Idempotency .config ()
407483 .withPersistenceStore (store )
408- .withConfig (IdempotencyConfig .builder ().build ()
409- ).configure ();
484+ .withConfig (IdempotencyConfig .builder ().build ()).configure ();
410485
411486 // WHEN
412487 IdempotencyInternalFunctionInvalid function = new IdempotencyInternalFunctionInvalid ();
@@ -421,8 +496,7 @@ public void idempotencyOnSubMethodNotAnnotated_shouldThrowException() {
421496 public void idempotencyOnSubMethodVoid_shouldThrowException () {
422497 Idempotency .config ()
423498 .withPersistenceStore (store )
424- .withConfig (IdempotencyConfig .builder ().build ()
425- ).configure ();
499+ .withConfig (IdempotencyConfig .builder ().build ()).configure ();
426500
427501 // WHEN
428502 IdempotencyInternalFunctionVoid function = new IdempotencyInternalFunctionVoid ();
0 commit comments