1
0
mirror of synced 2024-11-22 01:13:49 +00:00

Implemented unit tests for QueueArguments attribute

Added proof-of-concept test for TapetiClient using Testcontainers.NET
Updated packages
This commit is contained in:
Mark van Renswoude 2022-11-21 16:59:09 +01:00
parent 7143ad3c2f
commit 178f0a4956
32 changed files with 615 additions and 46 deletions

View File

@ -7,10 +7,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" Version="6.2.0" /> <PackageReference Include="Autofac" Version="6.5.0" />
<PackageReference Include="Castle.Windsor" Version="5.1.1" /> <PackageReference Include="Castle.Windsor" Version="5.1.2" />
<PackageReference Include="Ninject" Version="3.3.4" /> <PackageReference Include="Ninject" Version="3.3.6" />
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
<PackageReference Include="Unity" Version="5.11.10" /> <PackageReference Include="Unity" Version="5.11.10" />
</ItemGroup> </ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -7,8 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" Version="6.2.0" /> <PackageReference Include="Autofac" Version="6.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -30,6 +30,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Castle.Windsor" Version="5.1.1" /> <PackageReference Include="Castle.Windsor" Version="5.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -30,6 +30,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -34,6 +34,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -32,7 +32,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Data.SqlClient" Version="4.8.2" /> <PackageReference Include="System.Data.SqlClient" Version="4.8.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -48,6 +48,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -35,7 +35,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Tapeti.Annotations" Version="3.0.0" /> <PackageReference Include="Tapeti.Annotations" Version="3.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -15,7 +15,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Ninject" Version="3.3.4" /> <PackageReference Include="Ninject" Version="3.3.6" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -30,6 +30,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" /> <PackageReference Include="Serilog" Version="2.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -34,6 +34,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SimpleInjector" Version="5.3.0" /> <PackageReference Include="SimpleInjector" Version="5.4.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -34,6 +34,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,79 @@
using System;
using System.Threading.Tasks;
using Docker.DotNet;
using Docker.DotNet.Models;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Configurations;
using DotNet.Testcontainers.Containers;
using Xunit;
namespace Tapeti.Tests.Client
{
[CollectionDefinition(Name)]
public sealed class RabbitMQCollection : ICollectionFixture<RabbitMQFixture>
{
public const string Name = "RabbitMQ";
}
public sealed class RabbitMQFixture : IAsyncLifetime
{
public static string RabbitMQUsername => "tapetitests";
public static string RabbitMQPassword => "topsecret1234";
public ushort RabbitMQPort { get; private set; }
public ushort RabbitMQManagementPort { get; private set; }
private TestcontainerMessageBroker testcontainers;
private const int DefaultRabbitMQPort = 5672;
private const int DefaultRabbitMQManagementPort = 15672;
private const string ImageName = "rabbitmq";
private const string ImageTag = "3.11.3-alpine";
public async Task InitializeAsync()
{
// Testcontainers does not seem to pull the image the first time.
// I didn't get it to work, even using WithImagePullPolicy from the latest beta.
// Note: running it the first time can take a while.
var client = new DockerClientConfiguration().CreateClient();
await client.Images.CreateImageAsync(
new ImagesCreateParameters
{
FromImage = ImageName,
Tag = ImageTag
},
null,
new Progress<JSONMessage>());
// If you get a "Sequence contains no elements" error here: make sure Docker Desktop is running
var testcontainersBuilder = new TestcontainersBuilder<RabbitMqTestcontainer>()
.WithMessageBroker(new RabbitMqTestcontainerConfiguration($"{ImageName}:{ImageTag}")
{
Username = RabbitMQUsername,
Password = RabbitMQPassword
})
.WithExposedPort(DefaultRabbitMQManagementPort)
.WithPortBinding(0, DefaultRabbitMQManagementPort);
testcontainers = testcontainersBuilder.Build();
await testcontainers.StartAsync();
RabbitMQPort = testcontainers.GetMappedPublicPort(DefaultRabbitMQPort);
RabbitMQManagementPort = testcontainers.GetMappedPublicPort(DefaultRabbitMQManagementPort);
}
public async Task DisposeAsync()
{
if (testcontainers != null)
await testcontainers.DisposeAsync();
}
}
}

View File

@ -0,0 +1,75 @@
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Tapeti.Connection;
using Tapeti.Tests.Mock;
using Xunit;
using Xunit.Abstractions;
namespace Tapeti.Tests.Client
{
[Collection(RabbitMQCollection.Name)]
public class TapetiClientTests
{
private readonly RabbitMQFixture fixture;
private readonly MockDependencyResolver dependencyResolver = new();
public TapetiClientTests(RabbitMQFixture fixture, ITestOutputHelper testOutputHelper)
{
this.fixture = fixture;
dependencyResolver.Set<ILogger>(new MockLogger(testOutputHelper));
}
[Fact]
public void Fixture()
{
fixture.RabbitMQPort.Should().BeGreaterThan(0);
fixture.RabbitMQManagementPort.Should().BeGreaterThan(0);
}
[Fact]
public async Task DynamicQueueDeclareNoPrefix()
{
var client = CreateCilent();
var queueName = await client.DynamicQueueDeclare(null, null, CancellationToken.None);
queueName.Should().NotBeNullOrEmpty();
await client.Close();
}
[Fact]
public async Task DynamicQueueDeclarePrefix()
{
var client = CreateCilent();
var queueName = await client.DynamicQueueDeclare("dynamicprefix", null, CancellationToken.None);
queueName.Should().StartWith("dynamicprefix");
await client.Close();
}
// TODO test the other methods
private TapetiClient CreateCilent()
{
return new TapetiClient(
new TapetiConfig.Config(dependencyResolver),
new TapetiConnectionParams
{
HostName = "127.0.0.1",
Port = fixture.RabbitMQPort,
ManagementPort = fixture.RabbitMQManagementPort,
Username = RabbitMQFixture.RabbitMQUsername,
Password = RabbitMQFixture.RabbitMQPassword,
PrefetchCount = 50
});
}
}
}

View File

@ -0,0 +1,29 @@
using JetBrains.Annotations;
using Tapeti.Config;
using Tapeti.Tests.Mock;
namespace Tapeti.Tests.Config
{
public class BaseControllerTest
{
protected readonly MockDependencyResolver DependencyResolver = new();
protected ITapetiConfig GetControllerConfig<[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] T>() where T : class
{
var configBuilder = new TapetiConfig(DependencyResolver);
configBuilder.EnableDeclareDurableQueues();
configBuilder.RegisterController(typeof(T));
var config = configBuilder.Build();
return config;
}
protected ITapetiConfigBindings GetControllerBindings<[MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] T>() where T : class
{
return GetControllerConfig<T>().Bindings;
}
}
}

View File

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using FluentAssertions.Execution;
using Moq;
using Tapeti.Annotations;
using Tapeti.Config;
using Tapeti.Connection;
using Xunit;
namespace Tapeti.Tests.Config
{
public class QueueArgumentsTest : BaseControllerTest
{
private static readonly MockRepository MoqRepository = new(MockBehavior.Strict);
private readonly Mock<ITapetiClient> client;
private readonly Dictionary<string, IReadOnlyDictionary<string, string>> declaredQueues = new();
public QueueArgumentsTest()
{
client = MoqRepository.Create<ITapetiClient>();
var routingKeyStrategy = MoqRepository.Create<IRoutingKeyStrategy>();
var exchangeStrategy = MoqRepository.Create<IExchangeStrategy>();
DependencyResolver.Set(routingKeyStrategy.Object);
DependencyResolver.Set(exchangeStrategy.Object);
routingKeyStrategy
.Setup(s => s.GetRoutingKey(typeof(TestMessage1)))
.Returns("testmessage1");
routingKeyStrategy
.Setup(s => s.GetRoutingKey(typeof(TestMessage2)))
.Returns("testmessage2");
exchangeStrategy
.Setup(s => s.GetExchange(It.IsAny<Type>()))
.Returns("exchange");
var queue = 0;
client
.Setup(c => c.DynamicQueueDeclare(null, It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<CancellationToken>()))
.Callback((string _, IReadOnlyDictionary<string, string> arguments, CancellationToken _) =>
{
queue++;
declaredQueues.Add($"queue-{queue}", arguments);
})
.ReturnsAsync(() => $"queue-{queue}");
client
.Setup(c => c.DurableQueueDeclare(It.IsAny<string>(), It.IsAny<IEnumerable<QueueBinding>>(), It.IsAny<IReadOnlyDictionary<string, string>>(), It.IsAny<CancellationToken>()))
.Callback((string queueName, IEnumerable<QueueBinding> _, IReadOnlyDictionary<string, string> arguments, CancellationToken _) =>
{
declaredQueues.Add(queueName, arguments);
})
.Returns(Task.CompletedTask);
client
.Setup(c => c.DynamicQueueBind(It.IsAny<string>(), It.IsAny<QueueBinding>(), It.IsAny<CancellationToken>()))
.Returns(Task.CompletedTask);
}
[Fact]
public async Task SingleQueueArguments()
{
var config = GetControllerConfig<TestController>();
var binding1 = config.Bindings.Single(b => b is IControllerMethodBinding cmb && cmb.Method.Name == "HandleMessage1");
binding1.Should().NotBeNull();
var binding2 = config.Bindings.Single(b => b is IControllerMethodBinding cmb && cmb.Method.Name == "HandleMessage2");
binding2.Should().NotBeNull();
var subscriber = new TapetiSubscriber(() => client.Object, config);
await subscriber.ApplyBindings();
declaredQueues.Should().HaveCount(1);
var arguments = declaredQueues["queue-1"];
arguments.Should().ContainKey("x-custom").WhoseValue.Should().Be("custom value");
arguments.Should().ContainKey("x-another").WhoseValue.Should().Be("another value");
arguments.Should().ContainKey("x-max-length").WhoseValue.Should().Be("100");
arguments.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be("100000");
arguments.Should().ContainKey("x-message-ttl").WhoseValue.Should().Be("4269");
arguments.Should().ContainKey("x-overflow").WhoseValue.Should().Be("reject-publish");
}
[Fact]
public async Task ConflictingDynamicQueueArguments()
{
var config = GetControllerConfig<ConflictingArgumentsTestController>();
var subscriber = new TapetiSubscriber(() => client.Object, config);
await subscriber.ApplyBindings();
declaredQueues.Should().HaveCount(2);
var arguments1 = declaredQueues["queue-1"];
arguments1.Should().ContainKey("x-max-length").WhoseValue.Should().Be("100");
var arguments2 = declaredQueues["queue-2"];
arguments2.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be("100000");
}
[Fact]
public async Task ConflictingDurableQueueArguments()
{
var config = GetControllerConfig<ConflictingArgumentsDurableQueueTestController>();
var testApplyBindings = () =>
{
var subscriber = new TapetiSubscriber(() => client.Object, config);
return subscriber.ApplyBindings();
};
using (new AssertionScope())
{
await testApplyBindings.Should().ThrowAsync<TopologyConfigurationException>();
declaredQueues.Should().HaveCount(0);
}
}
// ReSharper disable all
#pragma warning disable
private class TestMessage1
{
}
private class TestMessage2
{
}
[DynamicQueue]
[QueueArguments("x-custom", "custom value", "x-another", "another value", MaxLength = 100, MaxLengthBytes = 100000, MessageTTL = 4269, Overflow = RabbitMQOverflow.RejectPublish)]
private class TestController
{
public void HandleMessage1(TestMessage1 message)
{
}
public void HandleMessage2(TestMessage2 message)
{
}
}
[DynamicQueue]
[QueueArguments(MaxLength = 100)]
private class ConflictingArgumentsTestController
{
public void HandleMessage1(TestMessage1 message)
{
}
[QueueArguments(MaxLengthBytes = 100000)]
public void HandleMessage2(TestMessage1 message)
{
}
}
[DurableQueue("durable")]
[QueueArguments(MaxLength = 100)]
private class ConflictingArgumentsDurableQueueTestController
{
public void HandleMessage1(TestMessage1 message)
{
}
[QueueArguments(MaxLengthBytes = 100000)]
public void HandleMessage2(TestMessage1 message)
{
}
}
#pragma warning restore
// ReSharper restore all
}
}

View File

@ -0,0 +1,43 @@
using System.Linq;
using FluentAssertions;
using Tapeti.Annotations;
using Tapeti.Config;
using Xunit;
namespace Tapeti.Tests.Config
{
public class SimpleControllerTest : BaseControllerTest
{
[Fact]
public void RegisterController()
{
var bindings = GetControllerBindings<TestController>();
bindings.Should().HaveCount(1);
var handleSimpleMessageBinding = bindings.Single(b => b is IControllerMethodBinding cmb &&
cmb.Controller == typeof(TestController) &&
cmb.Method.Name == "HandleSimpleMessage");
handleSimpleMessageBinding.QueueType.Should().Be(QueueType.Dynamic);
}
// ReSharper disable all
#pragma warning disable
private class TestMessage
{
}
[DynamicQueue]
private class TestController
{
public void HandleSimpleMessage(TestMessage message)
{
}
}
#pragma warning restore
// ReSharper restore all
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
namespace Tapeti.Tests.Mock
{
public class MockDependencyResolver : IDependencyResolver
{
private readonly Dictionary<Type, object> container = new();
public void Set<TInterface>(TInterface instance)
{
container.Add(typeof(TInterface), instance);
}
public T Resolve<T>() where T : class
{
return (T)Resolve(typeof(T));
}
public object Resolve(Type type)
{
return container[type];
}
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Text;
using Tapeti.Config;
using Xunit.Abstractions;
namespace Tapeti.Tests.Mock
{
internal class MockLogger : IBindingLogger
{
private readonly ITestOutputHelper testOutputHelper;
public MockLogger(ITestOutputHelper testOutputHelper)
{
this.testOutputHelper = testOutputHelper;
}
public void Connect(IConnectContext connectContext)
{
testOutputHelper.WriteLine($"{(connectContext.IsReconnect ? "Reconnecting" : "Connecting")} to {connectContext.ConnectionParams.HostName}:{connectContext.ConnectionParams.Port}{connectContext.ConnectionParams.VirtualHost}");
}
public void ConnectFailed(IConnectFailedContext connectContext)
{
testOutputHelper.WriteLine($"Connection failed: {connectContext.Exception}");
}
public void ConnectSuccess(IConnectSuccessContext connectContext)
{
testOutputHelper.WriteLine($"{(connectContext.IsReconnect ? "Reconnected" : "Connected")} using local port {connectContext.LocalPort}");
}
public void Disconnect(IDisconnectContext disconnectContext)
{
testOutputHelper.WriteLine($"Connection closed: {(!string.IsNullOrEmpty(disconnectContext.ReplyText) ? disconnectContext.ReplyText : "<no reply text>")} (reply code: {disconnectContext.ReplyCode})");
}
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{
testOutputHelper.WriteLine(exception.Message);
}
public void QueueDeclare(string queueName, bool durable, bool passive)
{
testOutputHelper.WriteLine(passive
? $"Verifying durable queue {queueName}"
: $"Declaring {(durable ? "durable" : "dynamic")} queue {queueName}");
}
public void QueueExistsWarning(string queueName, IReadOnlyDictionary<string, string> existingArguments, IReadOnlyDictionary<string, string> arguments)
{
var argumentsText = new StringBuilder();
foreach (var pair in arguments)
{
if (argumentsText.Length > 0)
argumentsText.Append(", ");
argumentsText.Append($"{pair.Key} = {pair.Value}");
}
testOutputHelper.WriteLine($"Durable queue {queueName} exists with incompatible x-arguments ({argumentsText}) and will not be redeclared, queue will be consumed as-is");
}
public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
{
testOutputHelper.WriteLine($"Binding {queueName} to exchange {exchange} with routing key {routingKey}");
}
public void QueueUnbind(string queueName, string exchange, string routingKey)
{
testOutputHelper.WriteLine($"Removing binding for {queueName} to exchange {exchange} with routing key {routingKey}");
}
public void ExchangeDeclare(string exchange)
{
testOutputHelper.WriteLine($"Declaring exchange {exchange}");
}
public void QueueObsolete(string queueName, bool deleted, uint messageCount)
{
testOutputHelper.WriteLine(deleted
? $"Obsolete queue was deleted: {queueName}"
: $"Obsolete queue bindings removed: {queueName}, {messageCount} messages remaining");
}
}
}

View File

@ -15,10 +15,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.10.3" /> <PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"> <PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Testcontainers" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>

View File

@ -30,6 +30,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -30,6 +30,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -118,7 +118,7 @@ namespace Tapeti.Config
/// before the configuration is built. Implementations of ITapetiConfigBuilder should also implement this interface. /// before the configuration is built. Implementations of ITapetiConfigBuilder should also implement this interface.
/// Should not be used outside of Tapeti packages. /// Should not be used outside of Tapeti packages.
/// </summary> /// </summary>
public interface ITapetiConfigBuilderAccess public interface ITapetiConfigBuilderAccess : ITapetiConfigBuilder
{ {
/// <summary> /// <summary>
/// Provides access to the dependency resolver. /// Provides access to the dependency resolver.

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Tapeti.Tests")]

View File

@ -24,8 +24,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" /> <PackageReference Include="RabbitMQ.Client" Version="6.4.0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="7.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
@ -41,7 +41,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Tapeti.Annotations" Version="3.*-*" /> <PackageReference Include="Tapeti.Annotations" Version="3.*-*" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -15,7 +15,7 @@ namespace Tapeti
/// Default implementation of the Tapeti config builder. /// Default implementation of the Tapeti config builder.
/// Automatically registers the default middleware for injecting the message parameter and handling the return value. /// Automatically registers the default middleware for injecting the message parameter and handling the return value.
/// </summary> /// </summary>
public class TapetiConfig : ITapetiConfigBuilder, ITapetiConfigBuilderAccess public class TapetiConfig : ITapetiConfigBuilderAccess
{ {
private Config config; private Config config;
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new(); private readonly List<IControllerBindingMiddleware> bindingMiddleware = new();

View File

@ -38,7 +38,7 @@ namespace Tapeti
if (!controller.IsClass) if (!controller.IsClass)
throw new ArgumentException($"Controller {controller.Name} must be a class"); throw new ArgumentException($"Controller {controller.Name} must be a class");
var controllerQueueInfo = GetQueueInfo(controller); var controllerQueueInfo = GetQueueInfo(controller, null);
(builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller); (builderAccess.DependencyResolver as IDependencyContainer)?.RegisterController(controller);
var controllerIsObsolete = controller.GetCustomAttribute<ObsoleteAttribute>() != null; var controllerIsObsolete = controller.GetCustomAttribute<ObsoleteAttribute>() != null;
@ -79,7 +79,7 @@ namespace Tapeti
throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}"); throw new TopologyConfigurationException($"Method {method.Name} in controller {method.DeclaringType?.Name} has unknown parameters: {parameterNames}");
} }
var methodQueueInfo = GetQueueInfo(method) ?? controllerQueueInfo; var methodQueueInfo = GetQueueInfo(method, controllerQueueInfo);
if (methodQueueInfo is not { IsValid: true }) if (methodQueueInfo is not { IsValid: true })
throw new TopologyConfigurationException( throw new TopologyConfigurationException(
$"Method {method.Name} or controller {controller.Name} requires a queue attribute"); $"Method {method.Name} or controller {controller.Name} requires a queue attribute");
@ -129,23 +129,45 @@ namespace Tapeti
} }
private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member) private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member, ControllerMethodBinding.QueueInfo fallbackQueueInfo)
{ {
var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>(); var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>(); var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>();
var queueArgumentsAttribute = member.GetCustomAttribute<QueueArgumentsAttribute>();
if (dynamicQueueAttribute != null && durableQueueAttribute != null) if (dynamicQueueAttribute != null && durableQueueAttribute != null)
throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}"); throw new TopologyConfigurationException($"Cannot combine static and dynamic queue attributes on controller {member.DeclaringType?.Name} method {member.Name}");
if (dynamicQueueAttribute == null && durableQueueAttribute == null && (queueArgumentsAttribute == null || fallbackQueueInfo == null))
return fallbackQueueInfo;
QueueType queueType;
string name;
var queueArgumentsAttribute = member.GetCustomAttribute<QueueArgumentsAttribute>();
if (dynamicQueueAttribute != null) if (dynamicQueueAttribute != null)
return new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Dynamic, Name = dynamicQueueAttribute.Prefix, QueueArguments = GetQueueArguments(queueArgumentsAttribute) }; {
queueType = QueueType.Dynamic;
name = dynamicQueueAttribute.Prefix;
}
else if (durableQueueAttribute != null)
{
queueType = QueueType.Durable;
name = durableQueueAttribute.Name;
}
else
{
queueType = fallbackQueueInfo.QueueType;
name = fallbackQueueInfo.Name;
}
return durableQueueAttribute != null return new ControllerMethodBinding.QueueInfo
? new ControllerMethodBinding.QueueInfo { QueueType = QueueType.Durable, Name = durableQueueAttribute.Name, QueueArguments = GetQueueArguments(queueArgumentsAttribute) } {
: null; QueueType = queueType,
Name = name,
QueueArguments = GetQueueArguments(queueArgumentsAttribute) ?? fallbackQueueInfo?.QueueArguments
};
} }