11namespace ServiceControl . AcceptanceTests . Recoverability . ExternalIntegration
22{
3- using System ;
43 using System . Collections . Generic ;
4+ using System . Linq ;
55 using System . Threading . Tasks ;
66 using AcceptanceTesting ;
77 using AcceptanceTesting . EndpointTemplates ;
88 using Contracts ;
99 using NServiceBus ;
1010 using NServiceBus . AcceptanceTesting ;
1111 using NUnit . Framework ;
12- using ServiceBus . Management . Infrastructure . Settings ;
1312 using ServiceControl . MessageFailures ;
13+ using ServiceControl . MessageFailures . Api ;
1414 using JsonSerializer = System . Text . Json . JsonSerializer ;
1515
1616 class When_a_failed_message_is_resolved_by_edit_and_retry : ExternalIntegrationAcceptanceTest
1717 {
1818 [ Test ]
1919 public async Task Should_publish_notification ( )
2020 {
21- CustomConfiguration = config => config . OnEndpointSubscribed < Context > ( ( s , ctx ) =>
22- {
23- ctx . ExternalProcessorSubscribed = s . SubscriberReturnAddress . Contains ( nameof ( ExternalProcessor ) ) ;
24- } ) ;
25-
26- var context = await Define < Context > ( )
27- . WithEndpoint < ErrorSender > ( b => b . When ( session => Task . CompletedTask ) . DoNotFailOnErrorMessages ( ) )
28- . WithEndpoint < ExternalProcessor > ( b => b . When ( async ( bus , c ) =>
21+ var context = await Define < EditMessageResolutionContext > ( )
22+ . WithEndpoint < EditMessageResolutionReceiver > ( b => b . When ( async ( bus , c ) =>
2923 {
3024 await bus . Subscribe < MessageFailureResolvedByRetry > ( ) ;
25+ } ) . When ( c => c . SendLocal ( new EditResolutionMessage ( ) ) ) . DoNotFailOnErrorMessages ( ) )
26+ . Done ( async ctx =>
27+ {
28+ if ( ! ctx . OriginalMessageHandled )
29+ {
30+ return false ;
31+ }
3132
32- if ( c . HasNativePubSubSupport )
33+ if ( ! ctx . EditedMessage )
3334 {
34- c . ExternalProcessorSubscribed = true ;
35+ var allFailedMessages =
36+ await this . TryGet < IList < FailedMessageView > > ( $ "/api/errors/?status=unresolved") ;
37+ if ( ! allFailedMessages . HasResult )
38+ {
39+ return false ;
40+ }
41+
42+ if ( allFailedMessages . Item . Count != 1 )
43+ {
44+ return false ;
45+ }
46+
47+ ctx . OriginalMessageFailureId = allFailedMessages . Item . First ( ) . Id ;
48+
49+ ctx . EditedMessage = true ;
50+ string editedMessage = JsonSerializer . Serialize ( new EditResolutionMessage
51+ {
52+ HasBeenEdited = true
53+ } ) ;
54+
55+ SingleResult < FailedMessage > failedMessage =
56+ await this . TryGet < FailedMessage > ( $ "/api/errors/{ ctx . OriginalMessageFailureId } ") ;
57+
58+ var editModel = new EditMessageModel
59+ {
60+ MessageBody = editedMessage ,
61+ MessageHeaders = failedMessage . Item . ProcessingAttempts . Last ( ) . Headers
62+ } ;
63+ await this . Post ( $ "/api/edit/{ ctx . OriginalMessageFailureId } ", editModel ) ;
64+ return false ;
3565 }
36- } ) )
37- . Do ( "WaitUntilErrorsContainsFailedMessage" ,
38- async ctx => await this . TryGet < FailedMessage > ( $ "/api/errors/{ ctx . FailedMessageId } ") != null )
39- . Do ( "WaitForExternalProcessorToSubscribe" ,
40- ctx => Task . FromResult ( ctx . ExternalProcessorSubscribed ) )
41- . Do ( "EditAndRetry" , async ctx =>
42- {
43- // First retrieve the original failed message to get all its headers
44- var originalFailedMessageResult = await this . TryGet < FailedMessage > ( $ "/api/errors/{ ctx . FailedMessageId } ") ;
45- var originalFailedMessage = originalFailedMessageResult . Item ;
4666
47- // Convert the original headers to Dictionary<string, string> for the edit payload
48- var originalHeaders = new Dictionary < string , string > ( ) ;
49- foreach ( var header in originalFailedMessage . ProcessingAttempts [ 0 ] . Headers )
67+ if ( ! ctx . EditedMessageHandled )
5068 {
51- originalHeaders [ header . Key ] = header . Value ;
69+ return false ;
5270 }
5371
54- // Prepare the edit payload with all original headers (locked headers unchanged, others can be modified)
55- var editPayload = new
72+ if ( ! ctx . MessageResolved )
5673 {
57- message_body = "{}" , // Empty JSON body for AMessage (ICommand with no properties)
58- message_headers = originalHeaders // Use all original headers to satisfy controller validation
59- } ;
74+ return false ;
75+ }
6076
61- await this . Post ( $ "/api/edit/{ ctx . FailedMessageId } ", editPayload ) ;
62- } )
63- . Do ( "EnsureRetried" , async ctx =>
64- {
65- return await this . TryGet < FailedMessage > ( $ "/api/errors/{ ctx . FailedMessageId } ",
66- e => e . Status == FailedMessageStatus . Resolved ) ;
67- } )
68- . Done ( ctx => ctx . EventDelivered ) //Done when sequence is finished
69- . Run ( ) ;
70-
71- var deserializedEvent = JsonSerializer . Deserialize < MessageFailureResolvedByRetry > ( context . Event ) ;
72- Assert . That ( deserializedEvent ? . FailedMessageId , Is . EqualTo ( context . FailedMessageId . ToString ( ) ) ) ;
77+ return true ;
78+ } ) . Run ( ) ;
79+
80+ Assert . That ( context . ResolvedMessageId , Is . EqualTo ( context . OriginalMessageFailureId ) ) ;
7381 }
7482
75- public class ExternalProcessor : EndpointConfigurationBuilder
83+
84+ class EditMessageResolutionContext : ScenarioContext
7685 {
77- public ExternalProcessor ( ) =>
78- EndpointSetup < DefaultServerWithoutAudit > ( c =>
79- {
80- var routing = c . ConfigureRouting ( ) ;
81- routing . RouteToEndpoint ( typeof ( MessageFailureResolvedByRetry ) . Assembly , Settings . DEFAULT_INSTANCE_NAME ) ;
82- } , publisherMetadata => { publisherMetadata . RegisterPublisherFor < MessageFailureResolvedByRetry > ( Settings . DEFAULT_INSTANCE_NAME ) ; } ) ;
86+ public bool OriginalMessageHandled { get ; set ; }
87+ public bool EditedMessage { get ; set ; }
88+ public string OriginalMessageFailureId { get ; set ; }
89+ public bool EditedMessageHandled { get ; set ; }
90+ public string ResolvedMessageId { get ; set ; }
91+ public bool MessageResolved { get ; set ; }
92+ }
8393
84- public class FailureHandler ( Context testContext ) : IHandleMessages < MessageFailureResolvedByRetry >
94+ class EditMessageResolutionReceiver : EndpointConfigurationBuilder
95+ {
96+ public EditMessageResolutionReceiver ( ) => EndpointSetup < DefaultServerWithoutAudit > ( c => c . NoRetries ( ) ) ;
97+
98+ class EditMessageResolutionHandler ( EditMessageResolutionContext testContext )
99+ : IHandleMessages < EditResolutionMessage > , IHandleMessages < MessageFailureResolvedByRetry >
85100 {
101+ public Task Handle ( EditResolutionMessage message , IMessageHandlerContext context )
102+ {
103+ if ( message . HasBeenEdited )
104+ {
105+ testContext . EditedMessageHandled = true ;
106+ return Task . CompletedTask ;
107+ }
108+
109+ testContext . OriginalMessageHandled = true ;
110+ throw new SimulatedException ( ) ;
111+ }
112+
86113 public Task Handle ( MessageFailureResolvedByRetry message , IMessageHandlerContext context )
87114 {
88- var serializedMessage = JsonSerializer . Serialize ( message ) ;
89- testContext . Event = serializedMessage ;
90- testContext . EventDelivered = true ;
115+ testContext . ResolvedMessageId = message . FailedMessageId ;
116+ testContext . MessageResolved = true ;
91117 return Task . CompletedTask ;
92118 }
93119 }
94120 }
121+
122+ class EditResolutionMessage : IMessage
123+ {
124+ public bool HasBeenEdited { get ; init ; }
125+ }
95126 }
96- }
127+ }
0 commit comments