Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@
<PackageVersion Include="Polly.Core" Version="8.5.2" />
<PackageVersion Include="PropertyChanged.Fody" Version="4.1.0" />
<PackageVersion Include="PropertyChanging.Fody" Version="1.30.3" />
<PackageVersion Include="PublicApiGenerator" Version="11.5.1" />
<PackageVersion Include="PublicApiGenerator" Version="11.5.3" />
<PackageVersion Include="RavenDB.Embedded" Version="6.2.6" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.1.63" />
<PackageVersion Include="Seq.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="ServiceControl.Contracts" Version="5.0.0" />
<PackageVersion Include="ServiceControl.Contracts" Version="5.1.0" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="8.0.1" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="8.0.1" />
<PackageVersion Include="System.DirectoryServices.AccountManagement" Version="8.0.1" />
Expand Down
20 changes: 20 additions & 0 deletions src/ServiceControl.AcceptanceTesting/NServiceBusAcceptanceTest.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Base class for all the NSB test that sets up our conventions
Expand Down Expand Up @@ -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)}"))}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@
<Compile Include="..\ServiceControl.Persistence.Tests.RavenDB\StopSharedDatabase.cs" />
</ItemGroup>

<ItemGroup>
<Folder Include="Shared\Recoverability\ExternalIntegration\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<string> GetOnlyFailedUnresolvedMessageId(this AcceptanceTest test)
{
var allFailedMessages =
await test.TryGet<IList<FailedMessageView>>($"/api/errors/?status=unresolved");
if (!allFailedMessages.HasResult)
{
return null;
}

if (allFailedMessages.Item.Count != 1)
{
return null;
}

return allFailedMessages.Item.First().Id;
}
}
Original file line number Diff line number Diff line change
@@ -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<EditMessageResolutionContext>((s, ctx) =>
{
ctx.ExternalProcessorSubscribed = s.SubscriberReturnAddress.Contains(nameof(MessageReceiver));
});

var context = await Define<EditMessageResolutionContext>()
.WithEndpoint<MessageReceiver>(b => b.When(async (bus, c) =>
{
await bus.Subscribe<MessageFailureResolvedByRetry>();
await bus.Subscribe<MessageFailed>();
await bus.Subscribe<MessageEditedAndRetried>();

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> failedMessage =
await this.TryGet<FailedMessage>($"/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<object>($"/api/errors/{ctx.EditedMessageFailureId}/retry");
return false;
}

if (ctx.MessageHandledCount != 3)
{
return false;
}

if (!ctx.MessageResolved || !ctx.EditAndRetryHandled || !ctx.MessageFailedResolved)
{
return false;
}

return true;
}).Run();

Assert.Multiple(() =>
{
Assert.That(context.ResolvedMessageId, Is.EqualTo(context.EditedMessageFailureId));
Assert.That(context.EditedMessageEditOf, Is.EqualTo(context.OriginalMessageFailureId));
Assert.That(context.MessageFailedFailedMessageIds.Count, Is.EqualTo(2));
Assert.That(context.MessageFailedFailedMessageIds, Is.Unique);
Assert.That(context.MessageFailedFailedMessageIds, Has.Some.EqualTo(context.OriginalMessageFailureId));
Assert.That(context.RetryFailedMessageId, Is.EqualTo(context.OriginalMessageFailureId));
Assert.That(context.MessageFailedFailedMessageIds, Has.Some.EqualTo(context.EditedMessageFailureId));
});
}


public class EditMessageResolutionContext : ScenarioContext
{
public string OriginalMessageFailureId { get; set; }
public int MessageSentCount { get; set; }
public int MessageHandledCount { get; set; }
public string ResolvedMessageId { get; set; }
public string EditedMessageFailureId { get; set; }
public string EditedMessageEditOf { get; set; }
public bool ExternalProcessorSubscribed { get; set; }
public bool MessageResolved { get; set; }
public bool MessageFailedResolved { get; set; }
public string RetryFailedMessageId { get; set; }
public bool EditAndRetryHandled { get; set; }
public List<string> MessageFailedFailedMessageIds { get; } = [];
}

public class MessageReceiver : EndpointConfigurationBuilder
{
public MessageReceiver() => EndpointSetup<DefaultServerWithoutAudit>(c => c.NoRetries());


public class EditMessageResolutionHandler(EditMessageResolutionContext testContext)
: IHandleMessages<EditResolutionMessage>, IHandleMessages<MessageFailureResolvedByRetry>, IHandleMessages<MessageFailed>, IHandleMessages<MessageEditedAndRetried>
{
public Task Handle(EditResolutionMessage message, IMessageHandlerContext context)
{
// First run - supposed to fail
if (testContext.MessageSentCount == 0)
{
testContext.MessageHandledCount = 1;
throw new SimulatedException();
}

// Second run - edit retry - supposed to fail
if (testContext.MessageSentCount == 1)
{
testContext.EditedMessageEditOf = context.MessageHeaders["ServiceControl.EditOf"];
testContext.MessageHandledCount = 2;
throw new SimulatedException();
}

// Last run - normal retry - supposed to succeed
testContext.MessageHandledCount = 3;
return Task.CompletedTask;
}

public Task Handle(MessageFailureResolvedByRetry message, IMessageHandlerContext context)
{
testContext.ResolvedMessageId = message.FailedMessageId;
testContext.MessageResolved = true;
return Task.CompletedTask;
}

public Task Handle(MessageFailed message, IMessageHandlerContext context)
{
testContext.MessageFailedFailedMessageIds.Add(message.FailedMessageId);
if (testContext.MessageFailedFailedMessageIds.Count == 2)
{
testContext.MessageFailedResolved = true;
}
return Task.CompletedTask;
}

public Task Handle(MessageEditedAndRetried message, IMessageHandlerContext context)
{
testContext.RetryFailedMessageId = message.FailedMessageId;
testContext.EditAndRetryHandled = true;
return Task.CompletedTask;
}
}
}

public class EditResolutionMessage : IMessage
{
}
}
}

Loading