@@ -230,5 +230,101 @@ def test_sender_builder_rejects_bad_psbt(self):
230230 payjoin .SenderBuilder ("not-a-psbt" , uri )
231231
232232
233+ class TestRetryMetadata (unittest .TestCase ):
234+ @staticmethod
235+ def _ohttp_keys ():
236+ return payjoin .OhttpKeys .decode (
237+ bytes .fromhex (
238+ "01001604ba48c49c3d4a92a3ad00ecc63a024da10ced02180c73ec12d8a7ad2cc91bb483824fe2bee8d28bfe2eb2fc6453bc4d31cd851e8a6540e86c5382af588d370957000400010003"
239+ )
240+ )
241+
242+ def _receiver_builder (self , expiration = None ):
243+ builder = payjoin .ReceiverBuilder (
244+ "2MuyMrZHkbHbfjudmKUy45dU4P17pjG2szK" ,
245+ "https://example.com" ,
246+ self ._ohttp_keys (),
247+ )
248+ if expiration is not None :
249+ builder = builder .with_expiration (expiration )
250+ return builder
251+
252+ def test_sender_create_request_exposes_expiration_metadata (self ):
253+ recv_persister = InMemoryReceiverPersister (10 )
254+ receiver = self ._receiver_builder (expiration = 0 ).build ().save (recv_persister )
255+ uri = receiver .pj_uri ()
256+ sender = (
257+ payjoin .SenderBuilder (payjoin .original_psbt (), uri )
258+ .build_recommended (1000 )
259+ .save (InMemorySenderPersister (11 ))
260+ )
261+
262+ with self .assertRaises (payjoin .CreateRequestError ) as ctx :
263+ sender .create_v2_post_request ("https://example.com" )
264+
265+ self .assertFalse (ctx .exception .is_retryable ())
266+ self .assertIsInstance (ctx .exception .expired_at_unix_seconds (), int )
267+
268+ def test_receiver_error_exposes_expiration_metadata (self ):
269+ receiver = (
270+ self ._receiver_builder (expiration = 0 )
271+ .build ()
272+ .save (InMemoryReceiverPersister (12 ))
273+ )
274+
275+ with self .assertRaises (payjoin .ReceiverError .Protocol ) as ctx :
276+ receiver .create_poll_request ("https://example.com" )
277+
278+ protocol_error = ctx .exception [0 ]
279+ self .assertFalse (protocol_error .is_retryable ())
280+ self .assertIsInstance (protocol_error .expired_at_unix_seconds (), int )
281+
282+ def test_sender_persisted_error_keeps_retryable_transport_signal (self ):
283+ recv_persister = InMemoryReceiverPersister (13 )
284+ receiver = self ._receiver_builder ().build ().save (recv_persister )
285+ uri = receiver .pj_uri ()
286+ sender = (
287+ payjoin .SenderBuilder (payjoin .original_psbt (), uri )
288+ .build_recommended (1000 )
289+ .save (InMemorySenderPersister (14 ))
290+ )
291+ request = sender .create_v2_post_request ("https://example.com" )
292+ transition = sender .process_response (b"" , request .ohttp_ctx )
293+
294+ with self .assertRaises (payjoin .SenderPersistedError .EncapsulationError ) as ctx :
295+ transition .save (InMemorySenderPersister (15 ))
296+
297+ self .assertTrue (ctx .exception [0 ].is_retryable ())
298+
299+ def test_replay_errors_expose_expiration_metadata (self ):
300+ recv_persister = InMemoryReceiverPersister (16 )
301+ self ._receiver_builder (expiration = 0 ).build ().save (recv_persister )
302+
303+ with self .assertRaises (payjoin .ReceiverReplayError ) as recv_ctx :
304+ payjoin .replay_receiver_event_log (recv_persister )
305+
306+ self .assertFalse (recv_ctx .exception .is_retryable ())
307+ self .assertIsInstance (recv_ctx .exception .expired_at_unix_seconds (), int )
308+
309+ sender_persister = InMemorySenderPersister (17 )
310+ receiver = (
311+ self ._receiver_builder (expiration = 0 )
312+ .build ()
313+ .save (InMemoryReceiverPersister (18 ))
314+ )
315+ uri = receiver .pj_uri ()
316+ (
317+ payjoin .SenderBuilder (payjoin .original_psbt (), uri )
318+ .build_recommended (1000 )
319+ .save (sender_persister )
320+ )
321+
322+ with self .assertRaises (payjoin .SenderReplayError ) as send_ctx :
323+ payjoin .replay_sender_event_log (sender_persister )
324+
325+ self .assertFalse (send_ctx .exception .is_retryable ())
326+ self .assertIsInstance (send_ctx .exception .expired_at_unix_seconds (), int )
327+
328+
233329if __name__ == "__main__" :
234330 unittest .main ()
0 commit comments