@@ -811,9 +811,9 @@ func TestMessageHandler_PublishAndLogError(t *testing.T) {
811811
812812func TestManualDelivery_Success (t * testing.T ) {
813813 // Test scenario:
814- // - Manual delivery succeeds
815- // - Should cancel any pending retries
816- // - Should be acked
814+ // - Automatic delivery fails and schedules a retry
815+ // - Manual retry succeeds and cancels the pending retry
816+ // - Verifies the canceled ID matches the scheduled ID
817817
818818 // Setup test data
819819 tenant := models.Tenant {ID : idgen .String ()}
@@ -823,15 +823,20 @@ func TestManualDelivery_Success(t *testing.T) {
823823 event := testutil .EventFactory .Any (
824824 testutil .EventFactory .WithTenantID (tenant .ID ),
825825 testutil .EventFactory .WithDestinationID (destination .ID ),
826- testutil .EventFactory .WithEligibleForRetry (true ), // even with retry enabled
826+ testutil .EventFactory .WithEligibleForRetry (true ),
827827 )
828828
829829 // Setup mocks
830830 destGetter := & mockDestinationGetter {dest : & destination }
831831 eventGetter := newMockEventGetter ()
832832 eventGetter .registerEvent (& event )
833833 retryScheduler := newMockRetryScheduler ()
834- publisher := newMockPublisher ([]error {nil }) // successful publish
834+ publishErr := & destregistry.ErrDestinationPublishAttempt {
835+ Err : errors .New ("webhook returned 500" ),
836+ Provider : "webhook" ,
837+ Data : map [string ]interface {}{"error" : "server_error" },
838+ }
839+ publisher := newMockPublisher ([]error {publishErr , nil }) // first fails, second succeeds
835840 logPublisher := newMockLogPublisher (nil )
836841 alertMonitor := newMockAlertMonitor ()
837842
@@ -850,28 +855,39 @@ func TestManualDelivery_Success(t *testing.T) {
850855 idempotence .New (testutil .CreateTestRedisClient (t ), idempotence .WithSuccessfulTTL (24 * time .Hour )),
851856 )
852857
853- // Create and handle message
854- deliveryEvent := models.DeliveryEvent {
858+ // Step 1: Automatic delivery fails and schedules retry
859+ autoDeliveryEvent := models.DeliveryEvent {
855860 ID : idgen .DeliveryEvent (),
856861 Event : event ,
857862 DestinationID : destination .ID ,
858- Manual : true , // Manual delivery
863+ Manual : false ,
859864 }
860- mockMsg , msg := newDeliveryMockMessage (deliveryEvent )
861-
862- // Handle message
863- err := handler .Handle (context .Background (), msg )
865+ _ , autoMsg := newDeliveryMockMessage (autoDeliveryEvent )
866+ _ = handler .Handle (context .Background (), autoMsg )
867+
868+ require .Len (t , retryScheduler .taskIDs , 1 , "should schedule one retry" )
869+ scheduledRetryID := retryScheduler .taskIDs [0 ]
870+
871+ // Step 2: Manual retry succeeds and cancels pending retry
872+ manualDeliveryEvent := models.DeliveryEvent {
873+ ID : idgen .DeliveryEvent (), // New delivery event ID
874+ Event : event , // Same event
875+ DestinationID : destination .ID , // Same destination
876+ Manual : true ,
877+ }
878+ mockMsg , manualMsg := newDeliveryMockMessage (manualDeliveryEvent )
879+ err := handler .Handle (context .Background (), manualMsg )
864880 require .NoError (t , err )
865881
866882 // Assert behavior
867883 assert .True (t , mockMsg .acked , "message should be acked" )
868884 assert .False (t , mockMsg .nacked , "message should not be nacked" )
869- assert .Equal (t , 1 , publisher .current , "should publish once " )
870- assert .Len (t , retryScheduler .canceled , 1 , "should cancel pending retries " )
871- assert . Equal ( t , deliveryEvent . GetRetryID (), retryScheduler . canceled [ 0 ], "should cancel with correct retry ID" )
872- require . Len ( t , logPublisher . deliveries , 1 , "should have one delivery" )
873- assert .Equal (t , models . DeliveryStatusSuccess , logPublisher . deliveries [0 ]. Delivery . Status , "delivery status should be OK" )
874- assertAlertMonitor ( t , alertMonitor , true , & destination , nil )
885+ assert .Equal (t , 2 , publisher .current , "should publish twice (auto + manual) " )
886+ require .Len (t , retryScheduler .canceled , 1 , "should cancel pending retry " )
887+
888+ // Key assertion: the canceled ID must match the scheduled ID
889+ assert .Equal (t , scheduledRetryID , retryScheduler . canceled [0 ],
890+ "manual retry must cancel the same retry ID that was scheduled by automatic delivery" )
875891}
876892
877893func TestManualDelivery_PublishError (t * testing.T ) {
@@ -1243,3 +1259,87 @@ func assertAlertMonitor(t *testing.T, m *mockAlertMonitor, success bool, destina
12431259 assert .Nil (t , attempt .DeliveryResponse , "alert attempt should not have data" )
12441260 }
12451261}
1262+
1263+ func TestMessageHandler_RetryID_MultipleDestinations (t * testing.T ) {
1264+ // Test scenario:
1265+ // - One event is delivered to TWO different destinations
1266+ // - Both deliveries fail with retryable errors
1267+ // - Each should schedule a retry with a UNIQUE task ID
1268+ //
1269+ // This verifies that retry scheduling works correctly when one event fans out to multiple
1270+ // destinations. Each destination must get its own retry with a unique task ID, otherwise
1271+ // retries overwrite each other in the scheduler and only the last destination gets retried.
1272+
1273+ // Setup test data
1274+ tenant := models.Tenant {ID : idgen .String ()}
1275+ destination1 := testutil .DestinationFactory .Any (
1276+ testutil .DestinationFactory .WithType ("webhook" ),
1277+ testutil .DestinationFactory .WithTenantID (tenant .ID ),
1278+ )
1279+ destination2 := testutil .DestinationFactory .Any (
1280+ testutil .DestinationFactory .WithType ("webhook" ),
1281+ testutil .DestinationFactory .WithTenantID (tenant .ID ),
1282+ )
1283+ event := testutil .EventFactory .Any (
1284+ testutil .EventFactory .WithTenantID (tenant .ID ),
1285+ testutil .EventFactory .WithEligibleForRetry (true ),
1286+ )
1287+
1288+ // Setup mocks
1289+ destGetter := newMockMultiDestinationGetter ()
1290+ destGetter .registerDestination (& destination1 )
1291+ destGetter .registerDestination (& destination2 )
1292+ eventGetter := newMockEventGetter ()
1293+ eventGetter .registerEvent (& event )
1294+ retryScheduler := newMockRetryScheduler ()
1295+ publishErr := & destregistry.ErrDestinationPublishAttempt {
1296+ Err : errors .New ("webhook returned 500" ),
1297+ Provider : "webhook" ,
1298+ Data : map [string ]interface {}{"error" : "server_error" },
1299+ }
1300+ publisher := newMockPublisher ([]error {publishErr , publishErr }) // Both fail
1301+ logPublisher := newMockLogPublisher (nil )
1302+ alertMonitor := newMockAlertMonitor ()
1303+
1304+ // Setup message handler
1305+ handler := deliverymq .NewMessageHandler (
1306+ testutil .CreateTestLogger (t ),
1307+ logPublisher ,
1308+ destGetter ,
1309+ eventGetter ,
1310+ publisher ,
1311+ testutil .NewMockEventTracer (nil ),
1312+ retryScheduler ,
1313+ & backoff.ConstantBackoff {Interval : 1 * time .Second },
1314+ 10 ,
1315+ alertMonitor ,
1316+ idempotence .New (testutil .CreateTestRedisClient (t ), idempotence .WithSuccessfulTTL (24 * time .Hour )),
1317+ )
1318+
1319+ // Create delivery events for SAME event to DIFFERENT destinations
1320+ deliveryEvent1 := models.DeliveryEvent {
1321+ ID : idgen .DeliveryEvent (),
1322+ Event : event ,
1323+ DestinationID : destination1 .ID ,
1324+ }
1325+ deliveryEvent2 := models.DeliveryEvent {
1326+ ID : idgen .DeliveryEvent (),
1327+ Event : event ,
1328+ DestinationID : destination2 .ID ,
1329+ }
1330+
1331+ // Handle both messages
1332+ _ , msg1 := newDeliveryMockMessage (deliveryEvent1 )
1333+ _ , msg2 := newDeliveryMockMessage (deliveryEvent2 )
1334+
1335+ _ = handler .Handle (context .Background (), msg1 )
1336+ _ = handler .Handle (context .Background (), msg2 )
1337+
1338+ // Assert: Both retries should be scheduled
1339+ require .Len (t , retryScheduler .taskIDs , 2 , "should schedule 2 retries" )
1340+
1341+ // Assert: Task IDs should be UNIQUE (this is the bug - currently they are the same)
1342+ assert .NotEqual (t , retryScheduler .taskIDs [0 ], retryScheduler .taskIDs [1 ],
1343+ "BUG: retry task IDs should be unique per destination, but both are: %s" ,
1344+ retryScheduler .taskIDs [0 ])
1345+ }
0 commit comments