diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 87dde27202..f866685c01 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -64,11 +64,11 @@
-
+
-
+
diff --git a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
index 35a82c7ece..21fe3d4287 100644
--- a/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
+++ b/src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
@@ -1,10 +1,13 @@
namespace ServiceControl.AcceptanceTesting
{
+ using System;
using System.Linq;
using System.Threading;
+ using NServiceBus.AcceptanceTesting;
using NServiceBus.AcceptanceTesting.Customization;
using NServiceBus.Logging;
using NUnit.Framework;
+ using NUnit.Framework.Internal;
///
/// Base class for all the NSB test that sets up our conventions
@@ -35,5 +38,22 @@ public void SetUp()
return testName + "." + endpointBuilder;
};
}
+
+ [TearDown]
+ public void TearDown()
+ {
+ if (!TestExecutionContext.CurrentContext.TryGetRunDescriptor(out var runDescriptor))
+ {
+ return;
+ }
+
+ var scenarioContext = runDescriptor.ScenarioContext;
+
+ TestContext.Out.WriteLine($@"Test settings:
+{string.Join(Environment.NewLine, runDescriptor.Settings.Select(setting => $" {setting.Key}: {setting.Value}"))}");
+
+ TestContext.Out.WriteLine($@"Context:
+{string.Join(Environment.NewLine, scenarioContext.GetType().GetProperties().Select(p => $"{p.Name} = {p.GetValue(scenarioContext, null)}"))}");
+ }
}
}
\ No newline at end of file
diff --git a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj
index 602a756222..3dc48639d3 100644
--- a/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj
+++ b/src/ServiceControl.AcceptanceTests.RavenDB/ServiceControl.AcceptanceTests.RavenDB.csproj
@@ -30,4 +30,8 @@
+
+
+
+
diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs
new file mode 100644
index 0000000000..262991a3b3
--- /dev/null
+++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/FailedMessageExtensions.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using ServiceControl.AcceptanceTesting;
+using ServiceControl.AcceptanceTests;
+using ServiceControl.MessageFailures.Api;
+
+public static class FailedMessageExtensions
+{
+ internal static async Task GetOnlyFailedUnresolvedMessageId(this AcceptanceTest test)
+ {
+ var allFailedMessages =
+ await test.TryGet>($"/api/errors/?status=unresolved");
+ if (!allFailedMessages.HasResult)
+ {
+ return null;
+ }
+
+ if (allFailedMessages.Item.Count != 1)
+ {
+ return null;
+ }
+
+ return allFailedMessages.Item.First().Id;
+ }
+}
\ No newline at end of file
diff --git a/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_edit_is_resolved_by_retry.cs b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_edit_is_resolved_by_retry.cs
new file mode 100644
index 0000000000..6441e3a5b1
--- /dev/null
+++ b/src/ServiceControl.AcceptanceTests/Recoverability/ExternalIntegration/When_a_failed_edit_is_resolved_by_retry.cs
@@ -0,0 +1,191 @@
+namespace ServiceControl.AcceptanceTests.Recoverability.ExternalIntegration
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using AcceptanceTesting;
+ using AcceptanceTesting.EndpointTemplates;
+ using Contracts;
+ using NServiceBus;
+ using NServiceBus.AcceptanceTesting;
+ using NUnit.Framework;
+ using ServiceControl.MessageFailures;
+ using ServiceControl.MessageFailures.Api;
+ using JsonSerializer = System.Text.Json.JsonSerializer;
+
+ class When_a_failed_edit_is_resolved_by_retry : AcceptanceTest
+ {
+ [Test]
+ public async Task Should_publish_notification()
+ {
+ CustomConfiguration = config => config.OnEndpointSubscribed((s, ctx) =>
+ {
+ ctx.ExternalProcessorSubscribed = s.SubscriberReturnAddress.Contains(nameof(MessageReceiver));
+ });
+
+ var context = await Define()
+ .WithEndpoint(b => b.When(async (bus, c) =>
+ {
+ await bus.Subscribe();
+ await bus.Subscribe();
+ await bus.Subscribe();
+
+ if (c.HasNativePubSubSupport)
+ {
+ c.ExternalProcessorSubscribed = true;
+ }
+ }).When(c => c.SendLocal(new EditResolutionMessage())).DoNotFailOnErrorMessages())
+ .Done(async ctx =>
+ {
+ if (!ctx.ExternalProcessorSubscribed)
+ {
+ return false;
+ }
+
+ // second message - edit & retry
+ if (ctx.MessageSentCount == 0 && ctx.MessageHandledCount == 1)
+ {
+ var failedMessagedId = await this.GetOnlyFailedUnresolvedMessageId();
+ if (failedMessagedId == null)
+ {
+ return false;
+ }
+
+ ctx.OriginalMessageFailureId = failedMessagedId;
+ ctx.MessageSentCount = 1;
+
+ string editedMessage = JsonSerializer.Serialize(new EditResolutionMessage
+ { });
+
+ SingleResult failedMessage =
+ await this.TryGet($"/api/errors/{ctx.OriginalMessageFailureId}");
+
+ var editModel = new EditMessageModel
+ {
+ MessageBody = editedMessage,
+ MessageHeaders = failedMessage.Item.ProcessingAttempts.Last().Headers
+ };
+ await this.Post($"/api/edit/{ctx.OriginalMessageFailureId}", editModel);
+ return false;
+ }
+
+ // third message - retry
+ if (ctx.MessageSentCount == 1 && ctx.MessageHandledCount == 2)
+ {
+ var failedMessageIdAfterId = await this.GetOnlyFailedUnresolvedMessageId();
+ if (failedMessageIdAfterId == null)
+ {
+ return false;
+ }
+
+ ctx.EditedMessageFailureId = failedMessageIdAfterId;
+ ctx.MessageSentCount = 2;
+
+ await this.Post