@@ -458,4 +458,125 @@ mod tests {
458458 assert_eq ! ( resp. status( ) , StatusCode :: OK ) ;
459459 assert_eq ! ( publisher. published_payloads( ) , vec![ Bytes :: new( ) ] ) ;
460460 }
461+
462+ // ── ack failure / timeout paths ──────────────────────────────────────────
463+
464+ mod ack_test_support {
465+ use super :: * ;
466+ use async_nats:: jetstream:: publish:: PublishAck ;
467+ use std:: sync:: Arc ;
468+ use std:: sync:: Mutex ;
469+ use trogon_nats:: mocks:: MockError ;
470+
471+ #[ derive( Clone ) ]
472+ enum AckBehavior {
473+ Fail ,
474+ Hang ,
475+ }
476+
477+ #[ derive( Clone ) ]
478+ pub struct AckFailPublisher {
479+ behavior : Arc < Mutex < AckBehavior > > ,
480+ }
481+
482+ impl AckFailPublisher {
483+ pub fn failing ( ) -> Self {
484+ Self {
485+ behavior : Arc :: new ( Mutex :: new ( AckBehavior :: Fail ) ) ,
486+ }
487+ }
488+
489+ pub fn hanging ( ) -> Self {
490+ Self {
491+ behavior : Arc :: new ( Mutex :: new ( AckBehavior :: Hang ) ) ,
492+ }
493+ }
494+ }
495+
496+ pub enum AckFuture {
497+ Fail ,
498+ Hang ,
499+ }
500+
501+ impl IntoFuture for AckFuture {
502+ type Output = Result < PublishAck , MockError > ;
503+ type IntoFuture = std:: pin:: Pin < Box < dyn Future < Output = Self :: Output > + Send > > ;
504+
505+ fn into_future ( self ) -> Self :: IntoFuture {
506+ match self {
507+ AckFuture :: Fail => {
508+ Box :: pin ( async { Err ( MockError ( "simulated ack failure" . to_string ( ) ) ) } )
509+ }
510+ AckFuture :: Hang => Box :: pin ( std:: future:: pending ( ) ) ,
511+ }
512+ }
513+ }
514+
515+ impl JetStreamPublisher for AckFailPublisher {
516+ type PublishError = MockError ;
517+ type AckFuture = AckFuture ;
518+
519+ async fn publish_with_headers < S : ToSubject + Send > (
520+ & self ,
521+ _subject : S ,
522+ _headers : async_nats:: HeaderMap ,
523+ _payload : Bytes ,
524+ ) -> Result < AckFuture , MockError > {
525+ let behavior = self . behavior . lock ( ) . unwrap ( ) . clone ( ) ;
526+ match behavior {
527+ AckBehavior :: Fail => Ok ( AckFuture :: Fail ) ,
528+ AckBehavior :: Hang => Ok ( AckFuture :: Hang ) ,
529+ }
530+ }
531+ }
532+ }
533+
534+ use ack_test_support:: AckFailPublisher ;
535+ use async_nats:: subject:: ToSubject ;
536+
537+ #[ tokio:: test]
538+ async fn ack_failure_returns_500 ( ) {
539+ let publisher = AckFailPublisher :: failing ( ) ;
540+
541+ let state = AppState {
542+ js : publisher,
543+ webhook_secret : None ,
544+ subject_prefix : "github" . to_string ( ) ,
545+ nats_ack_timeout : Duration :: from_secs ( 10 ) ,
546+ } ;
547+
548+ let app = Router :: new ( )
549+ . route ( "/webhook" , post ( handle_webhook :: < AckFailPublisher > ) )
550+ . with_state ( state) ;
551+
552+ let resp = app
553+ . oneshot ( webhook_request ( b"{}" , "push" , "del-9" , None ) )
554+ . await
555+ . unwrap ( ) ;
556+
557+ assert_eq ! ( resp. status( ) , StatusCode :: INTERNAL_SERVER_ERROR ) ;
558+ }
559+
560+ #[ tokio:: test]
561+ async fn ack_timeout_returns_500 ( ) {
562+ let publisher = AckFailPublisher :: hanging ( ) ;
563+
564+ let state = AppState {
565+ js : publisher,
566+ webhook_secret : None ,
567+ subject_prefix : "github" . to_string ( ) ,
568+ nats_ack_timeout : Duration :: from_millis ( 10 ) ,
569+ } ;
570+
571+ let app = Router :: new ( )
572+ . route ( "/webhook" , post ( handle_webhook :: < AckFailPublisher > ) )
573+ . with_state ( state) ;
574+
575+ let resp = app
576+ . oneshot ( webhook_request ( b"{}" , "push" , "del-10" , None ) )
577+ . await
578+ . unwrap ( ) ;
579+
580+ assert_eq ! ( resp. status( ) , StatusCode :: INTERNAL_SERVER_ERROR ) ;
581+ }
461582}
0 commit comments