diff --git a/docker-compose-localstack.yaml b/docker-compose-localstack.yaml new file mode 100644 index 0000000000..6efa721fd8 --- /dev/null +++ b/docker-compose-localstack.yaml @@ -0,0 +1,15 @@ +version: '3' + +services: + localstack: + image: localstack/localstack + environment: + # LocalStack configuration: https://docs.localstack.cloud/references/configuration/ + - "SERVICES=s3,sqs,sns,sts,dynamodb" + - "DEFAULT_REGION=eu-west-1" + - "DEBUG=1" + ports: + - "4566:4566" # LocalStack Gateway + - "4510-4559:4510-4559" # External services port range + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" \ No newline at end of file diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs new file mode 100644 index 0000000000..ec8e17b48c --- /dev/null +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/CommandHandlers/FarewellEventHandler.cs @@ -0,0 +1,24 @@ +using System; +using Greetings.Ports.Commands; +using Paramore.Brighter; + +namespace Greetings.Ports.CommandHandlers +{ + public class FarewellEventHandler : RequestHandler + { + public override FarewellEvent Handle(FarewellEvent @event) + { + Console.BackgroundColor = ConsoleColor.Blue; + Console.ForegroundColor = ConsoleColor.White; + + Console.WriteLine("Received Farewell. Message Follows"); + Console.WriteLine("----------------------------------"); + Console.WriteLine(@event.Farewell); + Console.WriteLine("----------------------------------"); + Console.WriteLine("Message Ends"); + + Console.ResetColor(); + return base.Handle(@event); + } + } +} diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs new file mode 100644 index 0000000000..8f107f3f87 --- /dev/null +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Commands/FarewellEvent.cs @@ -0,0 +1,19 @@ +using System; +using Paramore.Brighter; + +namespace Greetings.Ports.Commands +{ + public class FarewellEvent : Event + { + public FarewellEvent() : base(Guid.NewGuid()) + { + } + + public FarewellEvent(string farewell) : base(Guid.NewGuid()) + { + Farewell = farewell; + } + + public string Farewell { get; set; } + } +} diff --git a/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs new file mode 100644 index 0000000000..f59152f38d --- /dev/null +++ b/samples/TaskQueue/AWSTaskQueue/Greetings/Ports/Mappers/FarewellEventMessageMapper.cs @@ -0,0 +1,51 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Text.Json; +using Greetings.Ports.Commands; +using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Greetings.Ports.Mappers +{ + public class FarewellEventMessageMapper : IAmAMessageMapper + { + public IRequestContext Context { get; set; } + + public Message MapToMessage(FarewellEvent request, Publication publication) + { + var header = new MessageHeader(messageId: request.Id, topic: publication.Topic, messageType: MessageType.MT_EVENT); + var body = new MessageBody(JsonSerializer.Serialize(request, JsonSerialisationOptions.Options)); + var message = new Message(header, body); + return message; + } + + public FarewellEvent MapToRequest(Message message) + { + var greetingCommand = JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + + return greetingCommand; + } + } +} diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/Program.cs index 0478b71099..e52d0b8f77 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsPumper/Program.cs @@ -27,32 +27,47 @@ private static async Task Main(string[] args) var host = new HostBuilder() .ConfigureServices((hostContext, services) => - { - if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) { - var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) + { + var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.USEast1, + cfg => + { + var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); - var producerRegistry = new SnsProducerRegistryFactory( - awsConnection, - new SnsPublication[] - { - new SnsPublication + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + new SnsPublication[] { - Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()) + new SnsPublication + { + Topic = new RoutingKey(typeof(GreetingEvent).FullName + .ToValidSNSTopicName()) + }, + new SnsPublication + { + Topic = new RoutingKey( + typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } } - } - ).Create(); - - services.AddBrighter() - .UseExternalBus((configure) => - { - configure.ProducerRegistry = producerRegistry; - }) - .AutoFromAssemblies(typeof(GreetingEvent).Assembly); - } + ).Create(); - services.AddHostedService(); - } + services.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) + .AutoFromAssemblies(typeof(GreetingEvent).Assembly); + } + + services.AddHostedService(); + } ) .UseConsoleLifetime() .UseSerilog() diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/Program.cs index a55495f0c6..b694dc12e6 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsReceiverConsole/Program.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -58,21 +59,36 @@ public static async Task Main(string[] args) new ChannelName(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()), bufferSize: 10, - timeOut: TimeSpan.FromMilliseconds(20), + timeOut: TimeSpan.FromMilliseconds(20), + lockTimeout: 30), + new SqsSubscription(new SubscriptionName("paramore.example.farewell"), + new ChannelName(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + new RoutingKey(typeof(FarewellEvent).FullName.ToValidSNSTopicName(true)), + bufferSize: 10, + timeOut: TimeSpan.FromMilliseconds(20), lockTimeout: 30) }; //create the gateway if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) { - var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); + var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + var region = string.IsNullOrWhiteSpace(serviceURL) ? RegionEndpoint.EUWest1 : RegionEndpoint.USEast1; + var awsConnection = new AWSMessagingGatewayConnection(credentials, region, + cfg => + { + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); services.AddServiceActivator(options => - { - options.Subscriptions = subscriptions; - options.DefaultChannelFactory = new ChannelFactory(awsConnection); - }) - .AutoFromAssemblies(); + { + options.Subscriptions = subscriptions; + options.DefaultChannelFactory = new ChannelFactory(awsConnection); + }) + .AutoFromAssemblies(); } services.AddHostedService(); @@ -82,11 +98,6 @@ public static async Task Main(string[] args) .Build(); await host.RunAsync(); - - - - } } } - diff --git a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs index ff96fb3590..632152f373 100644 --- a/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/TaskQueue/AWSTaskQueue/GreetingsSender/Program.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2017 Ian Cooper @@ -22,6 +23,7 @@ THE SOFTWARE. */ #endregion +using System; using System.Transactions; using Amazon; using Amazon.Runtime.CredentialManagement; @@ -48,10 +50,18 @@ static void Main(string[] args) var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new SerilogLoggerFactory()); - + if (new CredentialProfileStoreChain().TryGetAWSCredentials("default", out var credentials)) { - var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); + var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + var region = string.IsNullOrWhiteSpace(serviceURL) ? RegionEndpoint.EUWest1 : RegionEndpoint.USEast1; + var awsConnection = new AWSMessagingGatewayConnection(credentials, region, cfg => + { + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); var producerRegistry = new SnsProducerRegistryFactory( awsConnection, @@ -64,7 +74,7 @@ static void Main(string[] args) } } ).Create(); - + serviceCollection.AddBrighter() .UseExternalBus((configure) => { @@ -76,7 +86,8 @@ static void Main(string[] args) var commandProcessor = serviceProvider.GetService(); - commandProcessor.Post(new GreetingEvent("Ian")); + commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); + commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs index 87a80ca9e9..a02f17fa30 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSClientFactory.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -31,68 +32,40 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MessagingGateway.AWSSQS; -internal class AWSClientFactory +internal class AWSClientFactory( + AWSCredentials credentials, + RegionEndpoint region, + Action? clientConfigAction) { - private readonly AWSCredentials _credentials; - private readonly RegionEndpoint _region; - private readonly Action? _clientConfigAction; - public AWSClientFactory(AWSMessagingGatewayConnection connection) + : this(connection.Credentials, connection.Region, connection.ClientConfigAction) { - _credentials = connection.Credentials; - _region = connection.Region; - _clientConfigAction = connection.ClientConfigAction; - } - - public AWSClientFactory(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction) - { - _credentials = credentials; - _region = region; - _clientConfigAction = clientConfigAction; } public AmazonSimpleNotificationServiceClient CreateSnsClient() { - var config = new AmazonSimpleNotificationServiceConfig - { - RegionEndpoint = _region - }; + var config = new AmazonSimpleNotificationServiceConfig { RegionEndpoint = region }; - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } + clientConfigAction?.Invoke(config); - return new AmazonSimpleNotificationServiceClient(_credentials, config); + return new AmazonSimpleNotificationServiceClient(credentials, config); } public AmazonSQSClient CreateSqsClient() { - var config = new AmazonSQSConfig - { - RegionEndpoint = _region - }; + var config = new AmazonSQSConfig { RegionEndpoint = region }; - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } + clientConfigAction?.Invoke(config); - return new AmazonSQSClient(_credentials, config); + return new AmazonSQSClient(credentials, config); } public AmazonSecurityTokenServiceClient CreateStsClient() { - var config = new AmazonSecurityTokenServiceConfig - { - RegionEndpoint = _region - }; + var config = new AmazonSecurityTokenServiceConfig { RegionEndpoint = region }; - if (_clientConfigAction != null) - { - _clientConfigAction(config); - } + clientConfigAction?.Invoke(config); - return new AmazonSecurityTokenServiceClient(_credentials, config); + return new AmazonSecurityTokenServiceClient(credentials, config); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs index 73e8cde782..88a62924bc 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSMessagingGateway.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -24,84 +25,449 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; +using Amazon.SQS; +using Amazon.SQS.Model; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; +using InvalidOperationException = System.InvalidOperationException; namespace Paramore.Brighter.MessagingGateway.AWSSQS; public class AWSMessagingGateway(AWSMessagingGatewayConnection awsConnection) { protected static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - + private readonly AWSClientFactory _awsClientFactory = new(awsConnection); protected readonly AWSMessagingGatewayConnection AwsConnection = awsConnection; - protected string? ChannelTopicArn; + + /// + /// The Channel Address + /// The Channel Address can be a Topic ARN or Queue Url + /// + protected string? ChannelAddress => ChannelTopicArn ?? ChannelQueueUrl; + + /// + /// The Channel Topic Arn + /// + protected string? ChannelTopicArn { get; set; } + + /// + /// The Channel Queue URL + /// + protected string? ChannelQueueUrl { get; set; } + + /// + /// The Channel Dead Letter Queue ARN + /// + protected string? ChannelDeadLetterQueueArn { get; set; } protected async Task EnsureTopicAsync( - RoutingKey topic, - TopicFindBy topicFindBy, - SnsAttributes? attributes, - OnMissingChannel makeTopic = OnMissingChannel.Create, + RoutingKey topic, + TopicFindBy topicFindBy, + SnsAttributes? attributes, + OnMissingChannel makeTopic = OnMissingChannel.Create, CancellationToken cancellationToken = default) { - //on validate or assume, turn a routing key into a topicARN - if ((makeTopic == OnMissingChannel.Assume) || (makeTopic == OnMissingChannel.Validate)) - await ValidateTopicAsync(topic, topicFindBy, cancellationToken); - else if (makeTopic == OnMissingChannel.Create) await CreateTopicAsync(topic, attributes); + var type = attributes?.Type ?? SnsSqsType.Standard; + ChannelTopicArn = makeTopic switch + { + //on validate or assume, turn a routing key into a topicARN + OnMissingChannel.Assume or OnMissingChannel.Validate => await ValidateTopicAsync(topic, topicFindBy, type, + cancellationToken), + OnMissingChannel.Create => await CreateTopicAsync(topic, attributes), + _ => ChannelAddress + }; + return ChannelTopicArn; } - private async Task CreateTopicAsync(RoutingKey topicName, SnsAttributes? snsAttributes) + private async Task CreateTopicAsync(RoutingKey topic, SnsAttributes? snsAttributes) { using var snsClient = _awsClientFactory.CreateSnsClient(); + + + var topicName = topic.Value; var attributes = new Dictionary(); if (snsAttributes != null) { - if (!string.IsNullOrEmpty(snsAttributes.DeliveryPolicy)) attributes.Add("DeliveryPolicy", snsAttributes.DeliveryPolicy); - if (!string.IsNullOrEmpty(snsAttributes.Policy)) attributes.Add("Policy", snsAttributes.Policy); + if (!string.IsNullOrEmpty(snsAttributes.DeliveryPolicy)) + { + attributes.Add("DeliveryPolicy", snsAttributes.DeliveryPolicy); + } + + if (!string.IsNullOrEmpty(snsAttributes.Policy)) + { + attributes.Add("Policy", snsAttributes.Policy); + } + + if (snsAttributes.Type == SnsSqsType.Fifo) + { + topicName = topic.ToValidSNSTopicName(true); + attributes.Add("FifoTopic", "true"); + if (snsAttributes.ContentBasedDeduplication) + { + attributes.Add("ContentBasedDeduplication", "true"); + } + } } var createTopicRequest = new CreateTopicRequest(topicName) { - Attributes = attributes, - Tags = new List {new Tag {Key = "Source", Value = "Brighter"}} + Attributes = attributes, Tags = [new Tag { Key = "Source", Value = "Brighter" }] }; - + //create topic is idempotent, so safe to call even if topic already exists var createTopic = await snsClient.CreateTopicAsync(createTopicRequest); - if (!string.IsNullOrEmpty(createTopic.TopicArn)) - ChannelTopicArn = createTopic.TopicArn; - else - throw new InvalidOperationException($"Could not create Topic topic: {topicName} on {AwsConnection.Region}"); + { + return createTopic.TopicArn; + } + + throw new InvalidOperationException( + $"Could not create Topic topic: {topic} on {AwsConnection.Region}"); } - private async Task ValidateTopicAsync(RoutingKey topic, TopicFindBy findTopicBy, CancellationToken cancellationToken = default) + private async Task ValidateTopicAsync(RoutingKey topic, TopicFindBy findTopicBy, SnsSqsType snsSqsType, + CancellationToken cancellationToken = default) { - IValidateTopic topicValidationStrategy = GetTopicValidationStrategy(findTopicBy); - (bool exists, string? topicArn) = await topicValidationStrategy.ValidateAsync(topic); + var topicValidationStrategy = GetTopicValidationStrategy(findTopicBy, snsSqsType); + var (exists, topicArn) = await topicValidationStrategy.ValidateAsync(topic, cancellationToken); + if (exists) - ChannelTopicArn = topicArn; - else - throw new BrokerUnreachableException( - $"Topic validation error: could not find topic {topic}. Did you want Brighter to create infrastructure?"); + { + return topicArn; + } + + throw new BrokerUnreachableException( + $"Topic validation error: could not find topic {topic}. Did you want Brighter to create infrastructure?"); + } + + private IValidateTopic GetTopicValidationStrategy(TopicFindBy findTopicBy, SnsSqsType type) + => findTopicBy switch + { + TopicFindBy.Arn => new ValidateTopicByArn(_awsClientFactory.CreateSnsClient()), + TopicFindBy.Name => new ValidateTopicByName(_awsClientFactory.CreateSnsClient(), type), + TopicFindBy.Convention => new ValidateTopicByArnConvention(AwsConnection.Credentials, + AwsConnection.Region, + AwsConnection.ClientConfigAction, + type), + _ => throw new ConfigurationException("Unknown TopicFindBy used to determine how to read RoutingKey") + }; + + + protected async Task EnsureQueueAsync( + string queue, + QueueFindBy queueFindBy, + SqsAttributes? sqsAttributes, + OnMissingChannel makeChannel = OnMissingChannel.Create, + CancellationToken cancellationToken = default) + { + var type = sqsAttributes?.Type ?? SnsSqsType.Standard; + ChannelQueueUrl = makeChannel switch + { + //on validate or assume, turn a routing key into a queueUrl + OnMissingChannel.Assume or OnMissingChannel.Validate => await ValidateQueueAsync(queue, queueFindBy, type, makeChannel, cancellationToken), + OnMissingChannel.Create => await CreateQueueAsync(queue, sqsAttributes, makeChannel, cancellationToken), + _ => ChannelQueueUrl + }; + + return ChannelQueueUrl; } - private IValidateTopic GetTopicValidationStrategy(TopicFindBy findTopicBy) + private async Task CreateQueueAsync( + string queueName, + SqsAttributes? sqsAttributes, + OnMissingChannel makeChannel, + CancellationToken cancellationToken) { - switch (findTopicBy) + if (sqsAttributes?.RedrivePolicy != null) + { + ChannelDeadLetterQueueArn = await CreateDeadLetterQueueAsync(sqsAttributes, cancellationToken); + } + + using var sqsClient = _awsClientFactory.CreateSqsClient(); + + + var tags = new Dictionary { { "Source", "Brighter" } }; + var attributes = new Dictionary(); + if (sqsAttributes != null) + { + if (sqsAttributes.RedrivePolicy != null) + { + var policy = new + { + maxReceiveCount = sqsAttributes.RedrivePolicy.MaxReceiveCount, + deadLetterTargetArn = ChannelDeadLetterQueueArn + }; + + attributes.Add(QueueAttributeName.RedrivePolicy, + JsonSerializer.Serialize(policy, JsonSerialisationOptions.Options)); + } + + attributes.Add(QueueAttributeName.DelaySeconds, sqsAttributes.DelaySeconds.ToString()); + attributes.Add(QueueAttributeName.MessageRetentionPeriod, sqsAttributes.MessageRetentionPeriod.ToString()); + attributes.Add(QueueAttributeName.ReceiveMessageWaitTimeSeconds, sqsAttributes.TimeOut.Seconds.ToString()); + attributes.Add(QueueAttributeName.VisibilityTimeout, sqsAttributes.LockTimeout.ToString()); + if (sqsAttributes.IAMPolicy != null) + { + attributes.Add(QueueAttributeName.Policy, sqsAttributes.IAMPolicy); + } + + if (sqsAttributes.Tags != null) + { + foreach (var tag in sqsAttributes.Tags) + { + tags.Add(tag.Key, tag.Value); + } + } + + if (sqsAttributes.Type == SnsSqsType.Fifo) + { + queueName = queueName.ToValidSQSQueueName(true); + + attributes.Add(QueueAttributeName.FifoQueue, "true"); + if (sqsAttributes.ContentBasedDeduplication) + { + attributes.Add(QueueAttributeName.ContentBasedDeduplication, "true"); + } + + if (sqsAttributes is { DeduplicationScope: not null, FifoThroughputLimit: not null }) + { + attributes.Add(QueueAttributeName.FifoThroughputLimit, + sqsAttributes.FifoThroughputLimit.Value.ToString()); + attributes.Add(QueueAttributeName.DeduplicationScope, sqsAttributes.DeduplicationScope switch + { + DeduplicationScope.MessageGroup => "messageGroup", + _ => "queue" + }); + } + } + } + + string queueUrl; + var createQueueRequest = new CreateQueueRequest(queueName) { Attributes = attributes, Tags = tags }; + try { - case TopicFindBy.Arn: - return new ValidateTopicByArn(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); - case TopicFindBy.Convention: - return new ValidateTopicByArnConvention(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); - case TopicFindBy.Name: - return new ValidateTopicByName(AwsConnection.Credentials, AwsConnection.Region, AwsConnection.ClientConfigAction); - default: - throw new ConfigurationException("Unknown TopicFindBy used to determine how to read RoutingKey"); + // create queue is idempotent, so safe to call even if queue already exists + var createQueueResponse = await sqsClient.CreateQueueAsync(createQueueRequest, cancellationToken); + queueUrl = createQueueResponse.QueueUrl; } + catch (QueueNameExistsException) + { + var response = await sqsClient.GetQueueUrlAsync(queueName, cancellationToken); + queueUrl = response.QueueUrl; + } + + if (string.IsNullOrEmpty(queueUrl)) + { + throw new InvalidOperationException($"Could not create Queue queue: {queueName} on {AwsConnection.Region}"); + } + + if (sqsAttributes == null || sqsAttributes.ChannelType == ChannelType.PubSub) + { + using var snsClient = _awsClientFactory.CreateSnsClient(); + await CheckSubscriptionAsync(makeChannel, ChannelTopicArn!, queueUrl, sqsAttributes, sqsClient, snsClient); + } + + return queueUrl; + } + + private async Task CreateDeadLetterQueueAsync( + SqsAttributes sqsAttributes, + CancellationToken cancellationToken) + { + using var sqsClient = _awsClientFactory.CreateSqsClient(); + + var queueName = sqsAttributes.RedrivePolicy!.DeadlLetterQueueName; + + var tags = new Dictionary { { "Source", "Brighter" } }; + var attributes = new Dictionary(); + if (sqsAttributes.Type == SnsSqsType.Fifo) + { + queueName = queueName.ToValidSQSQueueName(true); + + attributes.Add(QueueAttributeName.FifoQueue, "true"); + if (sqsAttributes.ContentBasedDeduplication) + { + attributes.Add(QueueAttributeName.ContentBasedDeduplication, "true"); + } + + if (sqsAttributes is { DeduplicationScope: not null, FifoThroughputLimit: not null }) + { + attributes.Add(QueueAttributeName.FifoThroughputLimit, + sqsAttributes.FifoThroughputLimit.Value.ToString()); + attributes.Add(QueueAttributeName.DeduplicationScope, sqsAttributes.DeduplicationScope switch + { + DeduplicationScope.MessageGroup => "messageGroup", + _ => "queue" + }); + } + } + + string queueUrl; + + try + { + var request = new CreateQueueRequest(queueName) { Attributes = attributes, Tags = tags }; + // create queue is idempotent, so safe to call even if queue already exists + var response = await sqsClient.CreateQueueAsync(request, cancellationToken); + + queueUrl = response.QueueUrl ?? throw new InvalidOperationException( + $"Could not find create DLQ, status: {response.HttpStatusCode}"); + } + catch (QueueNameExistsException) + { + var response = await sqsClient.GetQueueUrlAsync(queueName, cancellationToken); + queueUrl = response.QueueUrl; + } + + var attributesResponse = await sqsClient.GetQueueAttributesAsync( + new GetQueueAttributesRequest { QueueUrl = queueUrl, AttributeNames = [QueueAttributeName.QueueArn] }, + cancellationToken); + + if (attributesResponse.HttpStatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException( + $"Could not find ARN of DLQ, status: {attributesResponse.HttpStatusCode}"); + } + + return attributesResponse.QueueARN; + } + + private async Task ValidateQueueAsync(string queueName, + QueueFindBy findBy, + SnsSqsType type, + OnMissingChannel makeChannel, + CancellationToken cancellationToken) + { + var validationStrategy = GetQueueValidationStrategy(findBy, type); + var (exists, queueUrl) = await validationStrategy.ValidateAsync(queueName, cancellationToken); + + if (exists) + { + return queueUrl; + } + + if (makeChannel == OnMissingChannel.Assume) + { + return null; + } + + throw new QueueDoesNotExistException( + $"Queue validation error: could not find queue {queueName}. Did you want Brighter to create infrastructure?"); + } + + private IValidateQueue GetQueueValidationStrategy(QueueFindBy findQueueBy, SnsSqsType type) + => findQueueBy switch + { + QueueFindBy.Url => new ValidateQueueByUrl(_awsClientFactory.CreateSqsClient()), + QueueFindBy.Name => new ValidateQueueByName(_awsClientFactory.CreateSqsClient(), type), + _ => throw new ConfigurationException("Unknown TopicFindBy used to determine how to read RoutingKey") + }; + + private async Task CheckSubscriptionAsync(OnMissingChannel makeSubscriptions, + string topicArn, + string queueUrl, + SqsAttributes? sqsAttributes, + AmazonSQSClient sqsClient, + AmazonSimpleNotificationServiceClient snsClient) + { + if (makeSubscriptions == OnMissingChannel.Assume) + { + return; + } + + if (!await SubscriptionExistsAsync(topicArn, queueUrl, sqsClient, snsClient)) + { + if (makeSubscriptions == OnMissingChannel.Validate) + { + throw new BrokerUnreachableException( + $"Subscription validation error: could not find subscription for {queueUrl}"); + } + + if (makeSubscriptions == OnMissingChannel.Create) + { + await SubscribeToTopicAsync(topicArn, queueUrl, sqsAttributes, sqsClient, snsClient); + } + } + } + + private async Task SubscribeToTopicAsync( + string topicArn, + string queueUrl, + SqsAttributes? sqsAttributes, + AmazonSQSClient sqsClient, + AmazonSimpleNotificationServiceClient snsClient) + { + var arn = await snsClient.SubscribeQueueAsync(topicArn, sqsClient, queueUrl); + if (string.IsNullOrEmpty(arn)) + { + throw new InvalidOperationException( + $"Could not subscribe to topic: {topicArn} from queue: {queueUrl} in region {AwsConnection.Region}"); + } + + var response = await snsClient.SetSubscriptionAttributesAsync( + new SetSubscriptionAttributesRequest(arn, + "RawMessageDelivery", + sqsAttributes?.RawMessageDelivery.ToString()) + ); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException("Unable to set subscription attribute for raw message delivery"); + } + } + + private static async Task SubscriptionExistsAsync( + string topicArn, + string queueUrl, + AmazonSQSClient sqsClient, + AmazonSimpleNotificationServiceClient snsClient) + { + var queueArn = await GetQueueArnForChannelAsync(queueUrl, sqsClient); + + if (queueArn == null) + { + throw new BrokerUnreachableException($"Could not find queue ARN for queue {queueUrl}"); + } + + bool exists; + ListSubscriptionsByTopicResponse response; + do + { + response = await snsClient.ListSubscriptionsByTopicAsync( + new ListSubscriptionsByTopicRequest { TopicArn = topicArn }); + exists = response.Subscriptions.Any(sub => sub.Protocol.ToLower() == "sqs" && sub.Endpoint == queueArn); + } while (!exists && response.NextToken != null); + + return exists; + } + + /// + /// Gets the ARN of the queue for the channel. + /// Sync over async is used here; should be alright in context of channel creation. + /// + /// The queue url. + /// The SQS client. + /// The ARN of the queue. + private static async Task GetQueueArnForChannelAsync(string queueUrl, AmazonSQSClient sqsClient) + { + var result = await sqsClient.GetQueueAttributesAsync( + new GetQueueAttributesRequest { QueueUrl = queueUrl, AttributeNames = [QueueAttributeName.QueueArn] } + ); + + if (result.HttpStatusCode == HttpStatusCode.OK) + { + return result.QueueARN; + } + + return null; } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs index 222521766c..b5619b88a9 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/AWSNameExtensions.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,53 +20,59 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +public static class AWSNameExtensions { - public static class AWSNameExtensions + public static ChannelName ToValidSQSQueueName(this ChannelName? channelName, bool isFifo = false) { - public static ChannelName ToValidSQSQueueName(this ChannelName? channelName, bool isFifo = false) + if (channelName is null) { - if (channelName is null) - return new ChannelName(string.Empty); - - //SQS only allows 80 characters alphanumeric, hyphens, and underscores, but we might use a period in a - //default typename strategy - var name = channelName.Value; - name = name.Replace(".", "_"); - if (name.Length > 80) - name = name.Substring(0, 80); + return new ChannelName(string.Empty); + } - if (isFifo) - { - name = name + ".fifo"; - } + return new ChannelName(ToValidSQSQueueName(channelName.Value, isFifo)); + } + + public static RoutingKey ToValidSQSQueueName(this RoutingKey routingKey, bool isFifo = false) + => new(ToValidSQSQueueName(routingKey.Value, isFifo)); - return new ChannelName(name); - } + public static string ToValidSQSQueueName(this string queue, bool isFifo = false) + => Truncate(queue, isFifo, 80); + + public static RoutingKey ToValidSNSTopicName(this RoutingKey routingKey, bool isFifo = false) + => new(routingKey.Value.ToValidSNSTopicName(isFifo)); - public static RoutingKey ToValidSNSTopicName(this RoutingKey routingKey) + public static string ToValidSNSTopicName(this string topic, bool isFifo = false) + { + //SNS only topic names are limited to 256 characters. Alphanumeric characters plus hyphens (-) and + //underscores (_) are allowed. Topic names must be unique within an AWS account. + return Truncate(topic, isFifo, 256); + } + + private static string Truncate(string name, bool isFifo, int maxLength) + { + maxLength = isFifo switch + { + true when name.EndsWith("fifo") => name.Length - 5, + true => maxLength - 5, + false => maxLength + }; + + if (name.Length > maxLength) { - //SNS only topic names are limited to 256 characters. Alphanumeric characters plus hyphens (-) and - //underscores (_) are allowed. Topic names must be unique within an AWS account. - var topic = routingKey.Value; - topic = topic.Replace(".", "_"); - if (topic.Length > 256) - topic = topic.Substring(0, 256); - - return new RoutingKey(topic); + name = name.Substring(0, maxLength); } - - public static string ToValidSNSTopicName(this string topic) + + name = name.Replace('.', '_'); + if (isFifo) { - //SNS only topic names are limited to 256 characters. Alphanumeric characters plus hyphens (-) and - //underscores (_) are allowed. Topic names must be unique within an AWS account. - topic = topic.Replace(".", "_"); - if (topic.Length > 256) - topic = topic.Substring(0, 256); - - return topic; + name += ".fifo"; } - } + + return name; + } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs index 96ac49d665..94f2d2ae9a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelFactory.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; @@ -36,20 +38,17 @@ THE SOFTWARE. */ using Microsoft.Extensions.Logging; using Paramore.Brighter.Tasks; using Polly; -using Polly.Contrib.WaitAndRetry; using Polly.Retry; namespace Paramore.Brighter.MessagingGateway.AWSSQS; /// -/// The class is responsible for creating and managing SQS channels. +/// The class is responsible for creating and managing SNS/SQS channels. /// public class ChannelFactory : AWSMessagingGateway, IAmAChannelFactory { private readonly SqsMessageConsumerFactory _messageConsumerFactory; private SqsSubscription? _subscription; - private string? _queueUrl; - private string? _dlqARN; private readonly AsyncRetryPolicy _retryPolicy; /// @@ -62,12 +61,7 @@ public ChannelFactory(AWSMessagingGatewayConnection awsConnection) _messageConsumerFactory = new SqsMessageConsumerFactory(awsConnection); _retryPolicy = Policy .Handle() - .WaitAndRetryAsync(new[] - { - TimeSpan.FromSeconds(1), - TimeSpan.FromSeconds(5), - TimeSpan.FromSeconds(10) - }); + .WaitAndRetryAsync([TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)]); } /// @@ -77,8 +71,9 @@ public ChannelFactory(AWSMessagingGatewayConnection awsConnection) /// An SqsSubscription, the subscription parameter to create the channel with. /// An instance of . /// Thrown when the subscription is not an SqsSubscription. - public IAmAChannelSync CreateSyncChannel(Subscription subscription) => BrighterAsyncContext.Run(async () => await CreateSyncChannelAsync(subscription)); - + public IAmAChannelSync CreateSyncChannel(Subscription subscription) => + BrighterAsyncContext.Run(async () => await CreateSyncChannelAsync(subscription)); + /// /// Creates the input channel. /// @@ -88,7 +83,8 @@ public ChannelFactory(AWSMessagingGatewayConnection awsConnection) /// An SqsSubscription, the subscription parameter to create the channel with. /// An instance of . /// Thrown when the subscription is not an SqsSubscription. - public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) => BrighterAsyncContext.Run(async () => await CreateAsyncChannelAsync(subscription)); + public IAmAChannelAsync CreateAsyncChannel(Subscription subscription) => + BrighterAsyncContext.Run(async () => await CreateAsyncChannelAsync(subscription)); /// /// Creates the input channel. @@ -97,19 +93,43 @@ public ChannelFactory(AWSMessagingGatewayConnection awsConnection) /// Cancels the creation operation /// An instance of . /// Thrown when the subscription is not an SqsSubscription. - public async Task CreateAsyncChannelAsync(Subscription subscription, CancellationToken ct = default) + public async Task CreateAsyncChannelAsync(Subscription subscription, + CancellationToken ct = default) { var channel = await _retryPolicy.ExecuteAsync(async () => { SqsSubscription? sqsSubscription = subscription as SqsSubscription; - _subscription = sqsSubscription ?? throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); + _subscription = sqsSubscription ?? + throw new ConfigurationException( + "We expect an SqsSubscription or SqsSubscription as a parameter"); + + + var isFifo = _subscription.SqsType == SnsSqsType.Fifo; + var routingKey = _subscription.ChannelName.Value.ToValidSQSQueueName(isFifo); + if (_subscription.ChannelType == ChannelType.PubSub) + { + var snsAttributes = _subscription.SnsAttributes ?? new SnsAttributes(); + snsAttributes.Type = _subscription.SqsType; + + await EnsureTopicAsync(_subscription.RoutingKey, + _subscription.FindTopicBy, + snsAttributes, + _subscription.MakeChannels, + ct); + + routingKey = _subscription.RoutingKey.ToValidSNSTopicName(isFifo); + } - await EnsureTopicAsync(_subscription.RoutingKey, _subscription.FindTopicBy, _subscription.SnsAttributes, _subscription.MakeChannels); - await EnsureQueueAsync(); + await EnsureQueueAsync( + _subscription.ChannelName.Value, + _subscription.FindQueueBy, + SqsAttributes.From(_subscription), + _subscription.MakeChannels, + ct); return new ChannelAsync( - subscription.ChannelName.ToValidSQSQueueName(), - subscription.RoutingKey.ToValidSNSTopicName(), + subscription.ChannelName.ToValidSQSQueueName(isFifo), + new RoutingKey(routingKey), _messageConsumerFactory.CreateAsync(subscription), subscription.BufferSize ); @@ -117,7 +137,7 @@ public async Task CreateAsyncChannelAsync(Subscription subscri return channel; } - + /// /// Deletes the queue. /// @@ -126,16 +146,16 @@ public async Task DeleteQueueAsync() if (_subscription?.ChannelName is null) return; - using var sqsClient = new AmazonSQSClient(AwsConnection.Credentials, AwsConnection.Region); - (bool exists, string? queueUrl) queueExists = await QueueExistsAsync(sqsClient, _subscription.ChannelName.ToValidSQSQueueName()); + using var sqsClient = new AWSClientFactory(AwsConnection).CreateSqsClient(); + (bool exists, string? queueUrl) queueExists = + await QueueExistsAsync(sqsClient, + _subscription.ChannelName.ToValidSQSQueueName(_subscription.SqsType == SnsSqsType.Fifo)); - if (queueExists.exists && queueExists.queueUrl != null) + if (queueExists is { exists: true, queueUrl: not null }) { try { - sqsClient.DeleteQueueAsync(queueExists.queueUrl) - .GetAwaiter() - .GetResult(); + await sqsClient.DeleteQueueAsync(queueExists.queueUrl); } catch (Exception) { @@ -151,11 +171,11 @@ public async Task DeleteTopicAsync() { if (_subscription == null) return; - + if (ChannelTopicArn == null) return; - using var snsClient = new AmazonSimpleNotificationServiceClient(AwsConnection.Credentials, AwsConnection.Region); + using var snsClient = new AWSClientFactory(AwsConnection).CreateSnsClient(); (bool exists, string? _) = await new ValidateTopicByArn(snsClient).ValidateAsync(ChannelTopicArn); if (exists) { @@ -170,21 +190,40 @@ public async Task DeleteTopicAsync() } } } - + private async Task CreateSyncChannelAsync(Subscription subscription) { var channel = await _retryPolicy.ExecuteAsync(async () => { SqsSubscription? sqsSubscription = subscription as SqsSubscription; - _subscription = sqsSubscription ?? throw new ConfigurationException("We expect an SqsSubscription or SqsSubscription as a parameter"); + _subscription = sqsSubscription ?? + throw new ConfigurationException( + "We expect an SqsSubscription or SqsSubscription as a parameter"); + var routingKey = _subscription.ChannelName.Value; - await EnsureTopicAsync(_subscription.RoutingKey, _subscription.FindTopicBy, _subscription.SnsAttributes, + var isFifo = _subscription.SqsType == SnsSqsType.Fifo; + if (_subscription.ChannelType == ChannelType.PubSub) + { + var snsAttributes = _subscription.SnsAttributes ?? new SnsAttributes(); + snsAttributes.Type = _subscription.SqsType; + + await EnsureTopicAsync(_subscription.RoutingKey, + _subscription.FindTopicBy, + snsAttributes, + _subscription.MakeChannels); + + routingKey = _subscription.RoutingKey.ToValidSNSTopicName(isFifo); + } + + await EnsureQueueAsync( + _subscription.ChannelName.Value, + _subscription.FindQueueBy, + SqsAttributes.From(_subscription), _subscription.MakeChannels); - await EnsureQueueAsync(); return new Channel( - subscription.ChannelName.ToValidSQSQueueName(), - subscription.RoutingKey.ToValidSNSTopicName(), + subscription.ChannelName.ToValidSQSQueueName(isFifo), + new RoutingKey(routingKey), _messageConsumerFactory.Create(subscription), subscription.BufferSize ); @@ -192,209 +231,12 @@ await EnsureTopicAsync(_subscription.RoutingKey, _subscription.FindTopicBy, _sub return channel; } - - private async Task EnsureQueueAsync() - { - if (_subscription is null) - throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); - - if (_subscription.MakeChannels == OnMissingChannel.Assume) - return; - - using var sqsClient = new AmazonSQSClient(AwsConnection.Credentials, AwsConnection.Region); - var queueName = _subscription.ChannelName.ToValidSQSQueueName(); - var topicName = _subscription.RoutingKey.ToValidSNSTopicName(); - - (bool exists, _) = await QueueExistsAsync(sqsClient, queueName); - if (!exists) - { - if (_subscription.MakeChannels == OnMissingChannel.Create) - { - if (_subscription.RedrivePolicy != null) - { - await CreateDLQAsync(sqsClient); - } - await CreateQueueAsync(sqsClient); - } - else if (_subscription.MakeChannels == OnMissingChannel.Validate) - { - var message = $"Queue does not exist: {queueName} for {topicName} on {AwsConnection.Region}"; - s_logger.LogDebug("Queue does not exist: {ChannelName} for {Topic} on {Region}", queueName, topicName, AwsConnection.Region); - throw new QueueDoesNotExistException(message); - } - } - else - { - s_logger.LogDebug("Queue exists: {ChannelName} subscribed to {Topic} on {Region}", queueName, topicName, AwsConnection.Region); - } - } - - private async Task CreateQueueAsync(AmazonSQSClient sqsClient) - { - if (_subscription is null) - throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); - - s_logger.LogDebug("Queue does not exist, creating queue: {ChannelName} subscribed to {Topic} on {Region}", _subscription.ChannelName.Value, _subscription.RoutingKey.Value, AwsConnection.Region); - _queueUrl = null; - try - { - var attributes = new Dictionary(); - if (_subscription.RedrivePolicy != null && _dlqARN != null) - { - var policy = new { maxReceiveCount = _subscription.RedrivePolicy.MaxReceiveCount, deadLetterTargetArn = _dlqARN }; - attributes.Add("RedrivePolicy", JsonSerializer.Serialize(policy, JsonSerialisationOptions.Options)); - } - - attributes.Add("DelaySeconds", _subscription.DelaySeconds.ToString()); - attributes.Add("MessageRetentionPeriod", _subscription.MessageRetentionPeriod.ToString()); - if (_subscription.IAMPolicy != null) attributes.Add("Policy", _subscription.IAMPolicy); - attributes.Add("ReceiveMessageWaitTimeSeconds", _subscription.TimeOut.Seconds.ToString()); - attributes.Add("VisibilityTimeout", _subscription.LockTimeout.ToString()); - - var tags = new Dictionary { { "Source", "Brighter" } }; - if (_subscription.Tags != null) - { - foreach (var tag in _subscription.Tags) - { - tags.Add(tag.Key, tag.Value); - } - } - - var request = new CreateQueueRequest(_subscription.ChannelName.Value) - { - Attributes = attributes, - Tags = tags - }; - var response = await sqsClient.CreateQueueAsync(request); - _queueUrl = response.QueueUrl; - - if (!string.IsNullOrEmpty(_queueUrl)) - { - s_logger.LogDebug("Queue created: {URL}", _queueUrl); - using var snsClient = new AmazonSimpleNotificationServiceClient(AwsConnection.Credentials, AwsConnection.Region); - await CheckSubscriptionAsync(_subscription.MakeChannels, sqsClient, snsClient); - } - else - { - throw new InvalidOperationException($"Could not create queue: {_subscription.ChannelName.Value} subscribed to {ChannelTopicArn} on {AwsConnection.Region}"); - } - } - catch (QueueDeletedRecentlyException ex) - { - var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; - s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message); - Thread.Sleep(TimeSpan.FromSeconds(30)); - throw new ChannelFailureException(error, ex); - } - catch (AmazonSQSException ex) - { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); - } - catch (HttpErrorResponseException ex) - { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); - } - } - - private async Task CreateDLQAsync(AmazonSQSClient sqsClient) - { - if (_subscription is null) - throw new InvalidOperationException("ChannelFactory: Subscription cannot be null"); - - if (_subscription.RedrivePolicy == null) - throw new InvalidOperationException("ChannelFactory: RedrivePolicy cannot be null when creating a DLQ"); - - try - { - var request = new CreateQueueRequest(_subscription.RedrivePolicy.DeadlLetterQueueName.Value); - var createDeadLetterQueueResponse = await sqsClient.CreateQueueAsync(request); - var queueUrl = createDeadLetterQueueResponse.QueueUrl; - - if (!string.IsNullOrEmpty(queueUrl)) - { - var attributesRequest = new GetQueueAttributesRequest - { - QueueUrl = queueUrl, - AttributeNames = ["QueueArn"] - }; - var attributesResponse = await sqsClient.GetQueueAttributesAsync(attributesRequest); - - if (attributesResponse.HttpStatusCode != HttpStatusCode.OK) - throw new InvalidOperationException($"Could not find ARN of DLQ, status: {attributesResponse.HttpStatusCode}"); - - _dlqARN = attributesResponse.QueueARN; - } - else - throw new InvalidOperationException($"Could not find create DLQ, status: {createDeadLetterQueueResponse.HttpStatusCode}"); - } - catch (QueueDeletedRecentlyException ex) - { - var error = $"Could not create queue {_subscription.ChannelName.Value} because {ex.Message} waiting 60s to retry"; - s_logger.LogError(ex, "Could not create queue {ChannelName} because {ErrorMessage} waiting 60s to retry", _subscription.ChannelName.Value, ex.Message); - Thread.Sleep(TimeSpan.FromSeconds(30)); - throw new ChannelFailureException(error, ex); - } - catch (AmazonSQSException ex) - { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); - } - catch (HttpErrorResponseException ex) - { - var error = $"Could not create queue {_queueUrl} subscribed to topic {_subscription.RoutingKey.Value} in region {AwsConnection.Region.DisplayName} because {ex.Message}"; - s_logger.LogError(ex, "Could not create queue {URL} subscribed to topic {Topic} in region {Region} because {ErrorMessage}", _queueUrl, _subscription.RoutingKey.Value, AwsConnection.Region.DisplayName, ex.Message); - throw new InvalidOperationException(error, ex); - } - } - - private async Task CheckSubscriptionAsync(OnMissingChannel makeSubscriptions, AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) - { - if (makeSubscriptions == OnMissingChannel.Assume) - return; - - if (!await SubscriptionExistsAsync(sqsClient, snsClient)) - { - if (makeSubscriptions == OnMissingChannel.Validate) - { - throw new BrokerUnreachableException($"Subscription validation error: could not find subscription for {_queueUrl}"); - } - else if (makeSubscriptions == OnMissingChannel.Create) - { - await SubscribeToTopicAsync(sqsClient, snsClient); - } - } - } - - private async Task SubscribeToTopicAsync(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) - { - var arn = await snsClient.SubscribeQueueAsync(ChannelTopicArn, sqsClient, _queueUrl); - if (!string.IsNullOrEmpty(arn)) - { - var response = await snsClient.SetSubscriptionAttributesAsync( - new SetSubscriptionAttributesRequest(arn, "RawMessageDelivery", _subscription?.RawMessageDelivery.ToString()) - ); - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new InvalidOperationException("Unable to set subscription attribute for raw message delivery"); - } - } - else - { - throw new InvalidOperationException($"Could not subscribe to topic: {ChannelTopicArn} from queue: {_queueUrl} in region {AwsConnection.Region}"); - } - } - - private async Task<(bool exists, string? queueUrl)> QueueExistsAsync(AmazonSQSClient client, string? channelName) + private static async Task<(bool exists, string? queueUrl)> QueueExistsAsync(AmazonSQSClient client, string? channelName) { if (string.IsNullOrEmpty(channelName)) return (false, null); - + bool exists = false; string? queueUrl = null; try @@ -415,25 +257,32 @@ private async Task SubscribeToTopicAsync(AmazonSQSClient sqsClient, AmazonSimple exists = false; return true; } + return false; }); } + catch (QueueDoesNotExistException) + { + exists = false; + } return (exists, queueUrl); } - private async Task SubscriptionExistsAsync(AmazonSQSClient sqsClient, AmazonSimpleNotificationServiceClient snsClient) + private async Task SubscriptionExistsAsync(AmazonSQSClient sqsClient, + AmazonSimpleNotificationServiceClient snsClient) { string? queueArn = await GetQueueArnForChannelAsync(sqsClient); if (queueArn == null) - throw new BrokerUnreachableException($"Could not find queue ARN for queue {_queueUrl}"); + throw new BrokerUnreachableException($"Could not find queue ARN for queue {ChannelQueueUrl}"); bool exists = false; ListSubscriptionsByTopicResponse response; do { - response = await snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest { TopicArn = ChannelTopicArn }); + response = await snsClient.ListSubscriptionsByTopicAsync( + new ListSubscriptionsByTopicRequest { TopicArn = ChannelAddress }); exists = response.Subscriptions.Any(sub => (sub.Protocol.ToLower() == "sqs") && (sub.Endpoint == queueArn)); } while (!exists && response.NextToken != null); @@ -449,7 +298,7 @@ private async Task SubscriptionExistsAsync(AmazonSQSClient sqsClient, Amaz private async Task GetQueueArnForChannelAsync(AmazonSQSClient sqsClient) { var result = await sqsClient.GetQueueAttributesAsync( - new GetQueueAttributesRequest { QueueUrl = _queueUrl, AttributeNames = new List { "QueueArn" } } + new GetQueueAttributesRequest { QueueUrl = ChannelQueueUrl, AttributeNames = ["QueueArn"] } ); if (result.HttpStatusCode == HttpStatusCode.OK) @@ -470,13 +319,16 @@ private async Task UnsubscribeFromTopicAsync(AmazonSimpleNotificationServiceClie ListSubscriptionsByTopicResponse response; do { - response = await snsClient.ListSubscriptionsByTopicAsync(new ListSubscriptionsByTopicRequest { TopicArn = ChannelTopicArn }); + response = await snsClient.ListSubscriptionsByTopicAsync( + new ListSubscriptionsByTopicRequest { TopicArn = ChannelAddress }); foreach (var sub in response.Subscriptions) { - var unsubscribe = await snsClient.UnsubscribeAsync(new UnsubscribeRequest { SubscriptionArn = sub.SubscriptionArn }); + var unsubscribe = + await snsClient.UnsubscribeAsync(new UnsubscribeRequest { SubscriptionArn = sub.SubscriptionArn }); if (unsubscribe.HttpStatusCode != HttpStatusCode.OK) { - s_logger.LogError("Error unsubscribing from {TopicResourceName} for sub {ChannelResourceName}", ChannelTopicArn, sub.SubscriptionArn); + s_logger.LogError("Error unsubscribing from {TopicResourceName} for sub {ChannelResourceName}", + ChannelAddress, sub.SubscriptionArn); } } } while (response.NextToken != null); diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelType.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelType.cs new file mode 100644 index 0000000000..c36f80d6d9 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ChannelType.cs @@ -0,0 +1,17 @@ +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The Channel type +/// +public enum ChannelType +{ + /// + /// Use the Pub/Sub for routing key, aka SNS + /// + PubSub, + + /// + /// Use point-to-point for routing key, aka SQS + /// + PointToPoint +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/DeduplicationScope.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/DeduplicationScope.cs new file mode 100644 index 0000000000..a55c164431 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/DeduplicationScope.cs @@ -0,0 +1,18 @@ +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// For High throughput for FIFO queues +/// See: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/high-throughput-fifo.html +/// +public enum DeduplicationScope +{ + /// + /// The throughput configuration to be applied to message group + /// + MessageGroup, + + /// + /// The throughput configuration to be applied to the queue + /// + Queue +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderNames.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderNames.cs index 851d3cd701..32edcd263d 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderNames.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderNames.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,20 +20,22 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +public static class HeaderNames { - public static class HeaderNames - { - public static readonly string Id = "id"; - public static string Topic = "topic"; - public static string ContentType = "content-type"; - public static readonly string CorrelationId = "correlation-id"; - public static readonly string HandledCount = "handled-count"; - public static readonly string MessageType = "message-type"; - public static readonly string Timestamp = "timestamp"; - public static readonly string ReplyTo = "reply-to"; - public static string Bag = "bag"; - } + public const string Id = "id"; + public const string Topic = "topic"; + public const string ContentType = "content-type"; + public const string CorrelationId = "correlation-id"; + public const string HandledCount = "handled-count"; + public const string MessageType = "message-type"; + public const string Timestamp = "timestamp"; + public const string ReplyTo = "reply-to"; + public const string Subject = "subject"; + public const string Bag = "bag"; + public const string DeduplicationId = "messageDeduplicationId"; } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs index a6ff595de9..e250934d02 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/HeaderResult.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2018 Ian Cooper @@ -21,7 +22,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion + #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -42,9 +45,11 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; +using System.Diagnostics.CodeAnalysis; namespace Paramore.Brighter.MessagingGateway.AWSSQS { @@ -82,7 +87,9 @@ public HeaderResult Map(Func> map) /// Gets a value indicating whether this is success. /// /// true if success; otherwise, false. + [MemberNotNullWhen(true, nameof(Result))] public bool Success { get; } + /// /// Gets the result. /// @@ -99,12 +106,12 @@ public static HeaderResult Empty() { return new HeaderResult((TResult)(object)string.Empty, false); } - + if (typeof(TResult) == typeof(RoutingKey)) { return new HeaderResult((TResult)(object)RoutingKey.Empty, false); } - + return new HeaderResult(default(TResult), false); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateQueue.cs similarity index 78% rename from src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs rename to src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateQueue.cs index ca79ba0bf2..a81c2f642a 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessage.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/IValidateQueue.cs @@ -1,4 +1,4 @@ -#region Licence +#region Licence /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -21,17 +21,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion -using System; +using System.Threading; +using System.Threading.Tasks; -namespace Paramore.Brighter.MessagingGateway.AWSSQS -{ - /// - /// This class is used to deserialize a SNS backed SQS message - /// - public class SqsMessage - { - public Guid MessageId { get; set; } +namespace Paramore.Brighter.MessagingGateway.AWSSQS; - public string? Message { get; set; } - } +internal interface IValidateQueue +{ + Task<(bool, string?)> ValidateAsync(string queue, CancellationToken cancellationToken = default); } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/QueueFindBy.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/QueueFindBy.cs new file mode 100644 index 0000000000..e6d5fb8fdc --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/QueueFindBy.cs @@ -0,0 +1,13 @@ +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// How do we validate the queue - when we opt to validate or create (already exists) infrastructure +/// Relates to how we interpret the RoutingKey. Is it an Arn (0 or 1) or a name (2) +/// 0 - The queue is supplied as an url, and should be checked with a GetQueueAttributes call. May be any account. +/// 2 - The topic is supplies as a name, and should be checked by a GetQueueUrlAsync call. Must be in caller's account. +/// +public enum QueueFindBy +{ + Url, + Name +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs index 70c2454724..845151bdfc 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsAttributes.cs @@ -24,27 +24,36 @@ THE SOFTWARE. */ using System.Collections.Generic; using Amazon.SimpleNotificationService.Model; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +public class SnsAttributes { - public class SnsAttributes - { - /// - /// The policy that defines how Amazon SNS retries failed deliveries to HTTP/S endpoints - /// Ignored if TopicARN is set - /// - public string? DeliveryPolicy { get; set; } = null; - - /// - /// The JSON serialization of the topic's access control policy. - /// The policy that defines who can access your topic. By default, only the topic owner can publish or subscribe to the topic. - /// Ignored if TopicARN is set - /// - public string? Policy { get; set; } = null; + /// + /// The policy that defines how Amazon SNS retries failed deliveries to HTTP/S endpoints + /// Ignored if TopicARN is set + /// + public string? DeliveryPolicy { get; set; } = null; + + /// + /// The JSON serialization of the topic's access control policy. + /// The policy that defines who can access your topic. By default, only the topic owner can publish or subscribe to the topic. + /// Ignored if TopicARN is set + /// + public string? Policy { get; set; } = null; - /// - /// A list of resource tags to use when creating the publication - /// Ignored if TopicARN is set - /// - public List Tags => new List(); - } + /// + /// A list of resource tags to use when creating the publication + /// Ignored if TopicARN is set + /// + public List Tags => []; + + /// + /// The . + /// + public SnsSqsType Type { get; set; } = SnsSqsType.Standard; + + /// + /// Enable content based deduplication for Fifo Topics + /// + public bool ContentBasedDeduplication { get; set; } = true; } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs new file mode 100644 index 0000000000..1a8c222882 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducer.cs @@ -0,0 +1,180 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2022 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Tasks; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// Class SnsMessageProducer. +/// +public class SnsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, IAmAMessageProducerAsync +{ + private readonly SnsPublication _publication; + private readonly AWSClientFactory _clientFactory; + + /// + /// The publication configuration for this producer + /// + public Publication Publication => _publication; + + /// + /// The OTel Span we are writing Producer events too + /// + public Activity? Span { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// How do we connect to AWS in order to manage middleware + /// Configuration of a producer + public SnsMessageProducer(AWSMessagingGatewayConnection connection, SnsPublication publication) + : base(connection) + { + _publication = publication; + _clientFactory = new AWSClientFactory(connection); + + if (publication.TopicArn != null) + { + ChannelTopicArn = publication.TopicArn; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() { } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public ValueTask DisposeAsync() => new(); + + public bool ConfirmTopicExists(string? topic = null) => + BrighterAsyncContext.Run(async () => await ConfirmTopicExistsAsync(topic)); + + public async Task ConfirmTopicExistsAsync(string? topic = null, + CancellationToken cancellationToken = default) + { + //Only do this on first send for a topic for efficiency; won't auto-recreate when goes missing at runtime as a result + if (!string.IsNullOrEmpty(ChannelTopicArn)) + { + return true; + } + + RoutingKey? routingKey = null; + if (topic is not null) + { + routingKey = new RoutingKey(topic); + } + else if (_publication.Topic is not null) + { + routingKey = _publication.Topic; + } + + if (RoutingKey.IsNullOrEmpty(routingKey)) + { + throw new ConfigurationException("No topic specified for producer"); + } + + var topicArn = await EnsureTopicAsync( + routingKey, + _publication.FindTopicBy, + _publication.SnsAttributes, + _publication.MakeChannels, + cancellationToken); + + return !string.IsNullOrEmpty(topicArn); + } + + /// + /// Sends the specified message. + /// + /// The message. + /// Allows cancellation of the Send operation + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + { + s_logger.LogDebug( + "SNSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", + message.Header.Topic, message.Id, message.Body); + + await ConfirmTopicExistsAsync(message.Header.Topic, cancellationToken); + + if (string.IsNullOrEmpty(ChannelAddress)) + throw new InvalidOperationException( + $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body} as the topic does not exist"); + + using var client = _clientFactory.CreateSnsClient(); + var publisher = new SnsMessagePublisher(ChannelAddress!, client, + _publication.SnsAttributes?.Type ?? SnsSqsType.Standard); + var messageId = await publisher.PublishAsync(message); + + if (messageId == null) + throw new InvalidOperationException( + $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}"); + + s_logger.LogDebug( + "SNSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", + message.Header.Topic, message.Id, messageId); + } + + /// + /// Sends the specified message. + /// Sync over Async + /// + /// The message. + public void Send(Message message) => BrighterAsyncContext.Run(async () => await SendAsync(message)); + + /// + /// Sends the specified message, with a delay. + /// + /// The message. + /// The sending delay + /// Task. + public void SendWithDelay(Message message, TimeSpan? delay = null) + { + // SNS doesn't support publish with delay + Send(message); + } + + /// + /// Sends the specified message, with a delay + /// + /// The message + /// The sending delay + /// Cancels the send operation + /// + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, + CancellationToken cancellationToken = default) + { + // SNS doesn't support publish with delay + await SendAsync(message, cancellationToken); + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs index 65abc2ccd6..c504f54c52 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessageProducerFactory.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2024 Dominic Hickie @@ -19,70 +20,82 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System.Collections.Generic; using System.Threading.Tasks; -using Paramore.Brighter.Tasks; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +public class SnsMessageProducerFactory : IAmAMessageProducerFactory { - public class SnsMessageProducerFactory : IAmAMessageProducerFactory - { - private readonly AWSMessagingGatewayConnection _connection; - private readonly IEnumerable _publications; + private readonly AWSMessagingGatewayConnection _connection; + private readonly IEnumerable _publications; - /// - /// Creates a collection of SNS message producers from the SNS publication information - /// - /// The Connection to use to connect to AWS - /// The publications describing the SNS topics that we want to use - public SnsMessageProducerFactory( - AWSMessagingGatewayConnection connection, - IEnumerable publications) - { - _connection = connection; - _publications = publications; - } + /// + /// Creates a collection of SNS message producers from the SNS publication information + /// + /// The Connection to use to connect to AWS + /// The publications describing the SNS topics that we want to use + public SnsMessageProducerFactory( + AWSMessagingGatewayConnection connection, + IEnumerable publications) + { + _connection = connection; + _publications = publications; + } - /// - /// - /// Sync over async used here, alright in the context of producer creation - /// - public Dictionary Create() + /// + /// + /// Sync over async used here, alright in the context of producer creation + /// + public Dictionary Create() + { + var producers = new Dictionary(); + foreach (var p in _publications) { - var producers = new Dictionary(); - foreach (var p in _publications) + if (p.Topic is null) { - if (p.Topic is null) - throw new ConfigurationException($"Missing topic on Publication"); - - var producer = new SqsMessageProducer(_connection, p); - if (producer.ConfirmTopicExists()) - producers[p.Topic] = producer; - else - throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); + throw new ConfigurationException("Missing topic on Publication"); } - return producers; + var producer = new SnsMessageProducer(_connection, p); + if (producer.ConfirmTopicExists()) + { + producers[p.Topic] = producer; + } + else + { + throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); + } } - - public async Task> CreateAsync() + + return producers; + } + + /// + public async Task> CreateAsync() + { + var producers = new Dictionary(); + foreach (var p in _publications) { - var producers = new Dictionary(); - foreach (var p in _publications) + if (p.Topic is null) { - if (p.Topic is null) - throw new ConfigurationException($"Missing topic on Publication"); - - var producer = new SqsMessageProducer(_connection, p); - if (await producer.ConfirmTopicExistsAsync()) - producers[p.Topic] = producer; - else - throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); + throw new ConfigurationException("Missing topic on Publication"); } - return producers; + var producer = new SnsMessageProducer(_connection, p); + if (await producer.ConfirmTopicExistsAsync()) + { + producers[p.Topic] = producer; + } + else + { + throw new ConfigurationException($"Missing SNS topic: {p.Topic}"); + } } + + return producers; } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs new file mode 100644 index 0000000000..caf9bebd0d --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsMessagePublisher.cs @@ -0,0 +1,104 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2022 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +public class SnsMessagePublisher +{ + private readonly string _topicArn; + private readonly AmazonSimpleNotificationServiceClient _client; + private readonly SnsSqsType _snsSqsType; + + public SnsMessagePublisher(string topicArn, AmazonSimpleNotificationServiceClient client, SnsSqsType snsSqsType) + { + _topicArn = topicArn; + _client = client; + _snsSqsType = snsSqsType; + } + + public async Task PublishAsync(Message message) + { + var messageString = message.Body.Value; + var publishRequest = new PublishRequest(_topicArn, messageString, message.Header.Subject); + + var messageAttributes = new Dictionary + { + [HeaderNames.Id] = + new() { StringValue = Convert.ToString(message.Header.MessageId), DataType = "String" }, + [HeaderNames.Topic] = new() { StringValue = _topicArn, DataType = "String" }, + [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.CorrelationId] = + new() { StringValue = Convert.ToString(message.Header.CorrelationId), DataType = "String" }, + [HeaderNames.HandledCount] = + new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + [HeaderNames.MessageType] = + new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new() + { + StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" + } + }; + + if (_snsSqsType == SnsSqsType.Fifo) + { + publishRequest.MessageGroupId = message.Header.PartitionKey; + if (message.Header.Bag.TryGetValue(HeaderNames.DeduplicationId, out var deduplicationId)) + { + publishRequest.MessageDeduplicationId = (string)deduplicationId; + } + } + + if (!string.IsNullOrEmpty(message.Header.ReplyTo)) + { + messageAttributes.Add(HeaderNames.ReplyTo, + new MessageAttributeValue + { + StringValue = Convert.ToString(message.Header.ReplyTo), DataType = "String" + }); + } + + + //we can set up to 10 attributes; we have set 6 above, so use a single JSON object as the bag + var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); + messageAttributes[HeaderNames.Bag] = new() { StringValue = Convert.ToString(bagJson), DataType = "String" }; + publishRequest.MessageAttributes = messageAttributes; + + var response = await _client.PublishAsync(publishRequest); + if (response.HttpStatusCode is System.Net.HttpStatusCode.OK or System.Net.HttpStatusCode.Created + or System.Net.HttpStatusCode.Accepted) + { + return response.MessageId; + } + + return null; + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs index 4b6cd38c75..7f7a2f1179 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsProducerRegistryFactory.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,46 +20,54 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The SNS Message Producer registry factory +/// +public class SnsProducerRegistryFactory : IAmAProducerRegistryFactory { - public class SnsProducerRegistryFactory : IAmAProducerRegistryFactory - { - private readonly AWSMessagingGatewayConnection _connection; - private readonly IEnumerable _snsPublications; + private readonly AWSMessagingGatewayConnection _connection; + private readonly IEnumerable _snsPublications; - /// - /// Create a collection of producers from the publication information - /// - /// The Connection to use to connect to AWS - /// The publication describing the SNS topic that we want to use - public SnsProducerRegistryFactory( - AWSMessagingGatewayConnection connection, - IEnumerable snsPublications) - { - _connection = connection; - _snsPublications = snsPublications; - } + /// + /// Create a collection of producers from the publication information + /// + /// The Connection to use to connect to AWS + /// The publication describing the SNS topic that we want to use + public SnsProducerRegistryFactory( + AWSMessagingGatewayConnection connection, + IEnumerable snsPublications) + { + _connection = connection; + _snsPublications = snsPublications; + } - /// - /// Create a message producer for each publication, add it into the registry under the key of the topic - /// - /// - public IAmAProducerRegistry Create() - { - var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); - return new ProducerRegistry(producerFactory.Create()); - } + /// + /// Create a message producer for each publication, add it into the registry under the key of the topic + /// + /// The with . + public IAmAProducerRegistry Create() + { + var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); + return new ProducerRegistry(producerFactory.Create()); + } - public async Task CreateAsync(CancellationToken ct = default) - { - var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); - return new ProducerRegistry(await producerFactory.CreateAsync()); - } + /// + /// Create a message producer for each publication, add it into the registry under the key of the topic + /// + /// The . + /// The with . + public async Task CreateAsync(CancellationToken ct = default) + { + var producerFactory = new SnsMessageProducerFactory(_connection, _snsPublications); + return new ProducerRegistry(await producerFactory.CreateAsync()); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs index 0d9007d7a8..612a819229 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsPublication.cs @@ -21,29 +21,27 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #endregion -namespace Paramore.Brighter.MessagingGateway.AWSSQS -{ - public class SnsPublication : Publication - { - /// - /// Indicates how we should treat the routing key - /// TopicFindBy.Arn -> the routing key is an Arn - /// TopicFindBy.Convention -> The routing key is a name, but use convention to make an Arn for this account - /// TopicFindBy.Name -> Treat the routing key as a name & use ListTopics to find it (rate limited 30/s) - /// +namespace Paramore.Brighter.MessagingGateway.AWSSQS; - public TopicFindBy FindTopicBy { get; set; } = TopicFindBy.Convention; +public class SnsPublication : Publication +{ + /// + /// Indicates how we should treat the routing key + /// TopicFindBy.Arn -> the routing key is an Arn + /// TopicFindBy.Convention -> The routing key is a name, but use convention to make an Arn for this account + /// TopicFindBy.Name -> Treat the routing key as a name & use ListTopics to find it (rate limited 30/s) + /// + public TopicFindBy FindTopicBy { get; set; } = TopicFindBy.Convention; - /// - /// The attributes of the topic. If TopicARNs is set we will always assume that we do not - /// need to create or validate the SNS Topic - /// - public SnsAttributes? SnsAttributes { get; set; } + /// + /// The attributes of the topic. If TopicARNs is set we will always assume that we do not + /// need to create or validate the SNS Topic + /// + public SnsAttributes? SnsAttributes { get; set; } - /// - /// If we want to use topic Arns and not topics you need to supply the Arn to use for any message that you send to us, - /// as we use the topic from the header to dispatch to an Arn. - /// - public string? TopicArn { get; set; } - } + /// + /// If we want to use topic Arns and not topics you need to supply the Arn to use for any message that you send to us, + /// as we use the topic from the header to dispatch to an Arn. + /// + public string? TopicArn { get; set; } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsSqsType.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsSqsType.cs new file mode 100644 index 0000000000..9104a2294e --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SnsSqsType.cs @@ -0,0 +1,42 @@ +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// AWS offer two types of SQS. +/// For more information see +/// https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-queue-types.html +/// +public enum SnsSqsType +{ + /// + /// Standard queues support a very high, nearly unlimited number of API calls per second per + /// action (SendMessage, ReceiveMessage, or DeleteMessage). This high throughput makes them + /// ideal for use cases that require processing large volumes of messages quickly, such as + /// real-time data streaming or large-scale applications. While standard queues scale + /// automatically with demand, it's essential to monitor usage patterns to ensure optimal + /// performance, especially in regions with higher workloads. + /// + Standard, + + /// + /// FIFO (First-In-First-Out) queues have all the capabilities of the standard queues, + /// but are designed to enhance messaging between applications when the order of operations + /// and events is critical, or where duplicates can't be tolerated. + /// The most important features of FIFO queues are FIFO (First-In-First-Out) delivery and + /// exactly-once processing: + /// + /// + /// + /// The order in which messages are sent and received is strictly preserved and + /// a message is delivered once and remains unavailable until a consumer processes + /// and deletes it. + /// + /// + /// + /// + /// Duplicates aren't introduced into the queue. + /// + /// + /// + /// + Fifo +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsAttributes.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsAttributes.cs new file mode 100644 index 0000000000..2e0a8b2310 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsAttributes.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The SQS Attributes +/// +public class SqsAttributes +{ + /// + /// The routing key type. + /// + public ChannelType ChannelType { get; set; } + + /// + /// This governs how long, in seconds, a 'lock' is held on a message for one consumer + /// to process. SQS calls this the VisibilityTimeout + /// + public int LockTimeout { get; set; } + + /// + /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. + /// + public int DelaySeconds { get; set; } + + /// + /// The length of time, in seconds, for which Amazon SQS retains a message + /// + public int MessageRetentionPeriod { get; set; } + + /// + /// The JSON serialization of the queue's access control policy. + /// + public string? IAMPolicy { get; set; } + + /// + /// Indicate that the Raw Message Delivery setting is enabled or disabled + /// + public bool RawMessageDelivery { get; set; } + + /// + /// The policy that controls when we send messages to a DLQ after too many requeue attempts + /// + public RedrivePolicy? RedrivePolicy { get; set; } + + /// + /// Gets the timeout that we use to infer that nothing could be read from the channel i.e. is empty + /// or busy + /// + /// The timeout + public TimeSpan TimeOut { get; set; } + + /// + /// A list of resource tags to use when creating the queue + /// + public Dictionary? Tags { get; set; } + + /// + /// The AWS SQS type. + /// + public SnsSqsType Type { get; set; } + + /// + /// Enables or disable content-based deduplication, for Fifo queues. + /// + public bool ContentBasedDeduplication { get; set; } = true; + + /// + /// Specifies whether message deduplication occurs at the message group or queue level. + /// This configuration is used for high throughput for FIFO queues configuration + /// + public DeduplicationScope? DeduplicationScope { get; set; } + + /// + /// Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group + /// This configuration is used for high throughput for FIFO queues configuration + /// + public int? FifoThroughputLimit { get; set; } + + public static SqsAttributes From(SqsSubscription subscription) + { + return new SqsAttributes + { + ChannelType = subscription.ChannelType, + LockTimeout = subscription.LockTimeout, + DelaySeconds = subscription.DelaySeconds, + MessageRetentionPeriod = subscription.MessageRetentionPeriod, + IAMPolicy = subscription.IAMPolicy, + RawMessageDelivery = subscription.RawMessageDelivery, + RedrivePolicy = subscription.RedrivePolicy, + Tags = subscription.Tags, + Type = subscription.SqsType, + ContentBasedDeduplication = subscription.ContentBasedDeduplication, + DeduplicationScope = subscription.DeduplicationScope, + FifoThroughputLimit = subscription.FifoThroughputLimit, + TimeOut = subscription.TimeOut, + }; + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs index 0246ca06f7..d3f7eb7e6c 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsInlineMessageCreator.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,266 +20,300 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; using System.Collections.Generic; using System.Text.Json; +using Amazon; +using Amazon.SQS; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +internal class SqsInlineMessageCreator : SqsMessageCreatorBase, ISqsMessageCreator { - internal class SqsInlineMessageCreator : SqsMessageCreatorBase, ISqsMessageCreator - { - private static readonly ILogger s_logger= ApplicationLogging.CreateLogger(); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private Dictionary _messageAttributes = new Dictionary(); + private Dictionary _messageAttributes = new(); - public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) + public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) + { + var topic = HeaderResult.Empty(); + var messageId = HeaderResult.Empty(); + + Message message; + try { - var topic = HeaderResult.Empty(); - var messageId = HeaderResult.Empty(); - var contentType = HeaderResult.Empty(); - var correlationId = HeaderResult.Empty(); - var handledCount = HeaderResult.Empty(); - var messageType = HeaderResult.Empty(); - var timeStamp = HeaderResult.Empty(); - var receiptHandle = HeaderResult.Empty(); - var replyTo = HeaderResult.Empty(); - var subject = HeaderResult.Empty(); - - Message message; - try + var jsonDocument = JsonDocument.Parse(sqsMessage.Body); + _messageAttributes = ReadMessageAttributes(jsonDocument); + + topic = ReadTopic(); + messageId = ReadMessageId(); + var contentType = ReadContentType(); + var correlationId = ReadCorrelationId(); + var handledCount = ReadHandledCount(); + var messageType = ReadMessageType(); + var timeStamp = ReadTimestamp(); + var replyTo = ReadReplyTo(); + var subject = ReadMessageSubject(jsonDocument); + var receiptHandle = ReadReceiptHandle(sqsMessage); + var partitionKey = ReadPartitionKey(sqsMessage); + var deduplicationId = ReadDeduplicationId(sqsMessage); + + //TODO:CLOUD_EVENTS parse from headers + + var messageHeader = new MessageHeader( + messageId: messageId.Result ?? string.Empty, + topic: topic.Result ?? RoutingKey.Empty, + messageType: messageType.Result, + source: null, + type: "", + timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, + correlationId: correlationId.Success ? correlationId.Result : string.Empty, + replyTo: replyTo.Result is not null ? new RoutingKey(replyTo.Result) : RoutingKey.Empty, + contentType: contentType.Result ?? "plain/text", + handledCount: handledCount.Result, + dataSchema: null, + subject: subject.Result, + delayed: TimeSpan.Zero, + partitionKey: partitionKey.Result ?? string.Empty + ); + + message = new Message(messageHeader, ReadMessageBody(jsonDocument)); + + //deserialize the bag + var bag = ReadMessageBag(); + foreach (var key in bag.Keys) { - var jsonDocument = JsonDocument.Parse(sqsMessage.Body); - _messageAttributes = ReadMessageAttributes(jsonDocument); - - topic = ReadTopic(); - messageId = ReadMessageId() ; - contentType = ReadContentType(); - correlationId = ReadCorrelationId(); - handledCount = ReadHandledCount(); - messageType = ReadMessageType(); - timeStamp = ReadTimestamp(); - replyTo = ReadReplyTo(); - subject = ReadMessageSubject(jsonDocument); - receiptHandle = ReadReceiptHandle(sqsMessage); - - //TODO:CLOUD_EVENTS parse from headers - - var messageHeader = new MessageHeader( - messageId: messageId.Result ?? string.Empty, - topic: topic.Result ?? RoutingKey.Empty, - messageType: messageType.Result, - source: null, - type: "", - timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, - correlationId: correlationId.Success ? correlationId.Result : string.Empty, - replyTo: replyTo.Result is not null ? new RoutingKey(replyTo.Result) : RoutingKey.Empty, - contentType: contentType.Result ?? "plain/text", - handledCount: handledCount.Result, - dataSchema: null, - subject: subject.Result, - delayed: TimeSpan.Zero); - - message = new Message(messageHeader, ReadMessageBody(jsonDocument)); - - //deserialize the bag - var bag = ReadMessageBag(); - foreach (var key in bag.Keys) - { - message.Header.Bag.Add(key, bag[key]); - } + message.Header.Bag.Add(key, bag[key]); + } - if (receiptHandle.Success) - message.Header.Bag.Add("ReceiptHandle", sqsMessage.ReceiptHandle); + if (deduplicationId.Success) + { + message.Header.Bag[HeaderNames.DeduplicationId] = deduplicationId.Result; } - catch (Exception e) + + if (receiptHandle.Success) { - s_logger.LogWarning(e, "Failed to create message from Aws Sqs message"); - message = FailureMessage(topic, messageId); + message.Header.Bag.Add("ReceiptHandle", sqsMessage.ReceiptHandle); } - - - return message; } - - private static Dictionary ReadMessageAttributes(JsonDocument jsonDocument) + catch (Exception e) { - var messageAttributes = new Dictionary(); + s_logger.LogWarning(e, "Failed to create message from Aws Sqs message"); + message = FailureMessage(topic, messageId); + } - try - { - if (jsonDocument.RootElement.TryGetProperty("MessageAttributes", out var attributes)) - { - messageAttributes = JsonSerializer.Deserialize>( - attributes.GetRawText(), - JsonSerialisationOptions.Options); - } - } - catch (Exception ex) - { - s_logger.LogWarning($"Failed while deserializing Sqs Message body, ex: {ex}"); - } + return message; + } - return messageAttributes ?? new Dictionary(); - } + private static Dictionary ReadMessageAttributes(JsonDocument jsonDocument) + { + var messageAttributes = new Dictionary(); - private HeaderResult ReadContentType() + try { - if (_messageAttributes.TryGetValue(HeaderNames.ContentType, out var contentType)) + if (jsonDocument.RootElement.TryGetProperty("MessageAttributes", out var attributes)) { - return new HeaderResult(contentType.GetValueInString(), true); + messageAttributes = JsonSerializer.Deserialize>( + attributes.GetRawText(), + JsonSerialisationOptions.Options); } + } + catch (Exception ex) + { + s_logger.LogWarning($"Failed while deserializing Sqs Message body, ex: {ex}"); + } - return new HeaderResult(string.Empty, true); + return messageAttributes ?? new Dictionary(); + } + + private HeaderResult ReadContentType() + { + if (_messageAttributes.TryGetValue(HeaderNames.ContentType, out var contentType)) + { + return new HeaderResult(contentType.GetValueInString(), true); } - private Dictionary ReadMessageBag() + return new HeaderResult(string.Empty, true); + } + + private Dictionary ReadMessageBag() + { + if (_messageAttributes.TryGetValue(HeaderNames.Bag, out var headerBag)) { - if (_messageAttributes.TryGetValue(HeaderNames.Bag, out var headerBag)) + try { - try + var json = headerBag.GetValueInString(); + if (string.IsNullOrEmpty(json)) { - var json = headerBag.GetValueInString(); - if (string.IsNullOrEmpty(json)) - return new Dictionary(); - - var bag = JsonSerializer.Deserialize>( - json!, - JsonSerialisationOptions.Options); - - return bag ?? new Dictionary(); + return new Dictionary(); } - catch (Exception) - { - //suppress any errors in deserialization - } - } - return new Dictionary(); - } + var bag = JsonSerializer.Deserialize>(json!, + JsonSerialisationOptions.Options); - private HeaderResult ReadReplyTo() - { - if (_messageAttributes.TryGetValue(HeaderNames.ReplyTo, out var replyTo)) + return bag ?? new Dictionary(); + } + catch (Exception) { - return new HeaderResult(replyTo.GetValueInString(), true); + //suppress any errors in deserialization } + } - return new HeaderResult(string.Empty, true); + return new Dictionary(); + } + + private HeaderResult ReadReplyTo() + { + if (_messageAttributes.TryGetValue(HeaderNames.ReplyTo, out var replyTo)) + { + return new HeaderResult(replyTo.GetValueInString(), true); } - private HeaderResult ReadTimestamp() + return new HeaderResult(string.Empty, true); + } + + private HeaderResult ReadTimestamp() + { + if (_messageAttributes.TryGetValue(HeaderNames.Timestamp, out var timeStamp)) { - if (_messageAttributes.TryGetValue(HeaderNames.Timestamp, out var timeStamp)) + if (DateTime.TryParse(timeStamp.GetValueInString(), out var value)) { - if (DateTime.TryParse(timeStamp.GetValueInString(), out var value)) - { - return new HeaderResult(value, true); - } + return new HeaderResult(value, true); } - - return new HeaderResult(DateTime.UtcNow, true); } - private HeaderResult ReadMessageType() + return new HeaderResult(DateTime.UtcNow, true); + } + + private HeaderResult ReadMessageType() + { + if (_messageAttributes.TryGetValue(HeaderNames.MessageType, out var messageType)) { - if (_messageAttributes.TryGetValue(HeaderNames.MessageType, out var messageType)) + if (Enum.TryParse(messageType.GetValueInString(), out MessageType value)) { - if (Enum.TryParse(messageType.GetValueInString(), out MessageType value)) - { - return new HeaderResult(value, true); - } + return new HeaderResult(value, true); } - - return new HeaderResult(MessageType.MT_EVENT, true); } - private HeaderResult ReadHandledCount() + return new HeaderResult(MessageType.MT_EVENT, true); + } + + private HeaderResult ReadHandledCount() + { + if (_messageAttributes.TryGetValue(HeaderNames.HandledCount, out var handledCount)) { - if (_messageAttributes.TryGetValue(HeaderNames.HandledCount, out var handledCount)) + if (int.TryParse(handledCount.GetValueInString(), out var value)) { - if (int.TryParse(handledCount.GetValueInString(), out var value)) - { - return new HeaderResult(value, true); - } + return new HeaderResult(value, true); } - - return new HeaderResult(0, true); } - private HeaderResult ReadCorrelationId() + return new HeaderResult(0, true); + } + + private HeaderResult ReadCorrelationId() + { + if (_messageAttributes.TryGetValue(HeaderNames.CorrelationId, out var correlationId)) { - if (_messageAttributes.TryGetValue(HeaderNames.CorrelationId, out var correlationId)) - { - return new HeaderResult(correlationId.GetValueInString(), true); - } + return new HeaderResult(correlationId.GetValueInString(), true); + } - return new HeaderResult(string.Empty, true); + return new HeaderResult(string.Empty, true); + } + + private HeaderResult ReadMessageId() + { + if (_messageAttributes.TryGetValue(HeaderNames.Id, out var messageId)) + { + return new HeaderResult(messageId.GetValueInString(), true); } - private HeaderResult ReadMessageId() + return new HeaderResult(string.Empty, true); + } + + private HeaderResult ReadTopic() + { + if (_messageAttributes.TryGetValue(HeaderNames.Topic, out var topicArn)) { - if (_messageAttributes.TryGetValue(HeaderNames.Id, out var messageId)) + var topic = topicArn.GetValueInString() ?? string.Empty; + + if (Arn.TryParse(topic, out var arn)) { - return new HeaderResult(messageId.GetValueInString(), true); + return new HeaderResult(new RoutingKey(arn.Resource), true); } - return new HeaderResult(string.Empty, true); + var indexOf = topic.LastIndexOf('/'); + if (indexOf != -1) + { + return new HeaderResult(new RoutingKey(topic.Substring(indexOf + 1)), true); + } + + return new HeaderResult(new RoutingKey(topic), true); } - private HeaderResult ReadTopic() + return new HeaderResult(RoutingKey.Empty, true); + } + + private static HeaderResult ReadMessageSubject(JsonDocument jsonDocument) + { + try { - if (_messageAttributes.TryGetValue(HeaderNames.Topic, out var topicArn)) + if (jsonDocument.RootElement.TryGetProperty("Subject", out var value)) { - //we have an arn, and we want the topic - var s = topicArn.GetValueInString(); - if (string.IsNullOrEmpty(s)) - return new HeaderResult(RoutingKey.Empty, true); - - var arnElements = s!.Split(':'); - var topic = arnElements[(int)ARNAmazonSNS.TopicName]; - - return new HeaderResult(new RoutingKey(topic), true); + return new HeaderResult(value.GetString(), true); } - - return new HeaderResult(RoutingKey.Empty, true); } + catch (Exception ex) + { + s_logger.LogWarning($"Failed to parse Sqs Message Body to valid Json Document, ex: {ex}"); + } + + return new HeaderResult(null, true); + } - private static HeaderResult ReadMessageSubject(JsonDocument jsonDocument) + private static MessageBody ReadMessageBody(JsonDocument jsonDocument) + { + try { - try + if (jsonDocument.RootElement.TryGetProperty("Message", out var value)) { - if (jsonDocument.RootElement.TryGetProperty("Subject", out var value)) - { - return new HeaderResult(value.GetString(), true); - } + return new MessageBody(value.GetString()); } - catch (Exception ex) - { - s_logger.LogWarning($"Failed to parse Sqs Message Body to valid Json Document, ex: {ex}"); - } - - return new HeaderResult(null, true); } + catch (Exception ex) + { + s_logger.LogWarning($"Failed to parse Sqs Message Body to valid Json Document, ex: {ex}"); + } + + return new MessageBody(string.Empty); + } - private static MessageBody ReadMessageBody(JsonDocument jsonDocument) + private static HeaderResult ReadPartitionKey(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageGroupId, out var value)) { - try - { - if (jsonDocument.RootElement.TryGetProperty("Message", out var value)) - { - return new MessageBody(value.GetString()); - } - } - catch (Exception ex) - { - s_logger.LogWarning($"Failed to parse Sqs Message Body to valid Json Document, ex: {ex}"); - } + //we have an arn, and we want the topic + var messageGroupId = value; + return new HeaderResult(messageGroupId, true); + } + + return new HeaderResult(string.Empty, false); + } - return new MessageBody(string.Empty); + private static HeaderResult ReadDeduplicationId(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageDeduplicationId, out var value)) + { + //we have an arn, and we want the topic + var messageGroupId = value; + return new HeaderResult(messageGroupId, true); } + + return new HeaderResult(string.Empty, false); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs index 9793f429ba..161bed36d7 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumer.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2024 Ian Cooper @@ -19,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; @@ -39,7 +41,7 @@ namespace Paramore.Brighter.MessagingGateway.AWSSQS /// public class SqsMessageConsumer : IAmAMessageConsumerSync, IAmAMessageConsumerAsync { - private static readonly ILogger s_logger= ApplicationLogging.CreateLogger(); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private readonly AWSClientFactory _clientFactory; private readonly string _queueName; @@ -64,9 +66,9 @@ public SqsMessageConsumer(AWSMessagingGatewayConnection awsConnection, { if (string.IsNullOrEmpty(queueName)) throw new ConfigurationException("QueueName is mandatory"); - + _clientFactory = new AWSClientFactory(awsConnection); - _queueName = queueName!; + _queueName = queueName!; _batchSize = batchSize; _hasDlq = hasDLQ; _rawMessageDelivery = rawMessageDelivery; @@ -84,7 +86,8 @@ public SqsMessageConsumer(AWSMessagingGatewayConnection awsConnection, /// /// The message. /// Cancels the ackowledge operation - public async Task AcknowledgeAsync(Message message, CancellationToken cancellationToken = default(CancellationToken)) + public async Task AcknowledgeAsync(Message message, + CancellationToken cancellationToken = default(CancellationToken)) { if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object? value)) return; @@ -95,14 +98,19 @@ public SqsMessageConsumer(AWSMessagingGatewayConnection awsConnection, { using var client = _clientFactory.CreateSqsClient(); var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); - await client.DeleteMessageAsync(new DeleteMessageRequest(urlResponse.QueueUrl, receiptHandle), cancellationToken); + await client.DeleteMessageAsync(new DeleteMessageRequest(urlResponse.QueueUrl, receiptHandle), + cancellationToken); - s_logger.LogInformation("SqsMessageConsumer: Deleted the message {Id} with receipt handle {ReceiptHandle} on the queue {URL}", message.Id, receiptHandle, + s_logger.LogInformation( + "SqsMessageConsumer: Deleted the message {Id} with receipt handle {ReceiptHandle} on the queue {URL}", + message.Id, receiptHandle, urlResponse.QueueUrl); } catch (Exception exception) { - s_logger.LogError(exception, "SqsMessageConsumer: Error during deleting the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", message.Id, receiptHandle, _queueName); + s_logger.LogError(exception, + "SqsMessageConsumer: Error during deleting the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", + message.Id, receiptHandle, _queueName); throw; } } @@ -138,9 +146,9 @@ public SqsMessageConsumer(AWSMessagingGatewayConnection awsConnection, if (_hasDlq) { await client.ChangeMessageVisibilityAsync( - new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, 0), + new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, 0), cancellationToken - ); + ); } else { @@ -149,7 +157,9 @@ await client.ChangeMessageVisibilityAsync( } catch (Exception exception) { - s_logger.LogError(exception, "SqsMessageConsumer: Error during rejecting the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", message.Id, receiptHandle, _queueName); + s_logger.LogError(exception, + "SqsMessageConsumer: Error during rejecting the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", + message.Id, receiptHandle, _queueName); throw; } } @@ -194,7 +204,8 @@ await client.ChangeMessageVisibilityAsync( /// /// The timeout. AWS uses whole seconds. Anything greater than 0 uses long-polling. /// Cancel the receive operation - public async Task ReceiveAsync(TimeSpan? timeOut = null, CancellationToken cancellationToken = default(CancellationToken)) + public async Task ReceiveAsync(TimeSpan? timeOut = null, + CancellationToken cancellationToken = default(CancellationToken)) { AmazonSQSClient? client = null; Amazon.SQS.Model.Message[] sqsMessages; @@ -204,13 +215,15 @@ await client.ChangeMessageVisibilityAsync( var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); timeOut ??= TimeSpan.Zero; - s_logger.LogDebug("SqsMessageConsumer: Preparing to retrieve next message from queue {URL}", urlResponse.QueueUrl); + s_logger.LogDebug("SqsMessageConsumer: Preparing to retrieve next message from queue {URL}", + urlResponse.QueueUrl); var request = new ReceiveMessageRequest(urlResponse.QueueUrl) { MaxNumberOfMessages = _batchSize, WaitTimeSeconds = timeOut.Value.Seconds, - MessageAttributeNames = new List {"All"}, + MessageAttributeNames = ["All"], + MessageSystemAttributeNames = ["All"] }; var receiveResponse = await client.ReceiveMessageAsync(request, cancellationToken); @@ -229,7 +242,8 @@ await client.ChangeMessageVisibilityAsync( } catch (Exception e) { - s_logger.LogError(e, "SqsMessageConsumer: There was an error listening to queue {ChannelName} ", _queueName); + s_logger.LogError(e, "SqsMessageConsumer: There was an error listening to queue {ChannelName} ", + _queueName); throw; } finally @@ -239,15 +253,17 @@ await client.ChangeMessageVisibilityAsync( if (sqsMessages.Length == 0) { - return new[] {_noopMessage}; + return [_noopMessage]; } var messages = new Message[sqsMessages.Length]; for (int i = 0; i < sqsMessages.Length; i++) { var message = SqsMessageCreatorFactory.Create(_rawMessageDelivery).CreateMessage(sqsMessages[i]); - s_logger.LogInformation("SqsMessageConsumer: Received message from queue {ChannelName}, message: {1}{Request}", - _queueName, Environment.NewLine, JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); + s_logger.LogInformation( + "SqsMessageConsumer: Received message from queue {ChannelName}, message: {1}{Request}", + _queueName, Environment.NewLine, + JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); messages[i] = message; } @@ -269,7 +285,7 @@ public async Task RequeueAsync(Message message, TimeSpan? delay = null, { if (!message.Header.Bag.TryGetValue("ReceiptHandle", out object? value)) return false; - + delay ??= TimeSpan.Zero; var receiptHandle = value.ToString(); @@ -282,9 +298,9 @@ public async Task RequeueAsync(Message message, TimeSpan? delay = null, { var urlResponse = await client.GetQueueUrlAsync(_queueName, cancellationToken); await client.ChangeMessageVisibilityAsync( - new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, delay.Value.Seconds), + new ChangeMessageVisibilityRequest(urlResponse.QueueUrl, receiptHandle, delay.Value.Seconds), cancellationToken - ); + ); } s_logger.LogInformation("SqsMessageConsumer: re-queued the message {Id}", message.Id); @@ -293,7 +309,9 @@ await client.ChangeMessageVisibilityAsync( } catch (Exception exception) { - s_logger.LogError(exception, "SqsMessageConsumer: Error during re-queueing the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", message.Id, receiptHandle, _queueName); + s_logger.LogError(exception, + "SqsMessageConsumer: Error during re-queueing the message {Id} with receipt handle {ReceiptHandle} on the queue {ChannelName}", + message.Id, receiptHandle, _queueName); return false; } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs index 08468f5df2..e1219579a9 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageConsumerFactory.cs @@ -61,7 +61,7 @@ private SqsMessageConsumer CreateImpl(Subscription subscription) return new SqsMessageConsumer( awsConnection: _awsConnection, - queueName: subscription.ChannelName.ToValidSQSQueueName(), + queueName: subscription.ChannelName.ToValidSQSQueueName(sqsSubscription.SqsType == SnsSqsType.Fifo), batchSize: subscription.BufferSize, hasDLQ: sqsSubscription.RedrivePolicy == null, rawMessageDelivery: sqsSubscription.RawMessageDelivery diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs index 576b51825b..fea74f1a53 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageCreator.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,211 +20,274 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; using System.Collections.Generic; using System.Text.Json; +using Amazon; +using Amazon.SQS; using Amazon.SQS.Model; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using Paramore.Brighter.Transforms.Transformers; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +//arn:aws:sns:us-east-1:123456789012:my_corporate_topic:02034b43-fefa-4e07-a5eb-3be56f8c54ce +//https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns +internal enum ARNAmazonSNS +{ + Arn = 0, + Aws = 1, + Sns = 2, + Region = 3, + AccountId = 4, + TopicName = 5, + SubscriptionId = 6 +} + +internal class SqsMessageCreator : SqsMessageCreatorBase, ISqsMessageCreator { - //arn:aws:sns:us-east-1:123456789012:my_corporate_topic:02034b43-fefa-4e07-a5eb-3be56f8c54ce - //https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-arns - internal enum ARNAmazonSNS + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) { - Arn = 0, - Aws = 1, - Sns = 2, - Region = 3, - AccountId = 4, - TopicName = 5, - SubscriptionId = 6 + var topic = HeaderResult.Empty(); + var messageId = HeaderResult.Empty(); + + //TODO:CLOUD_EVENTS parse from headers + + Message message; + try + { + topic = ReadTopic(sqsMessage); + messageId = ReadMessageId(sqsMessage); + var contentType = ReadContentType(sqsMessage); + var correlationId = ReadCorrelationId(sqsMessage); + var handledCount = ReadHandledCount(sqsMessage); + var messageType = ReadMessageType(sqsMessage); + var timeStamp = ReadTimestamp(sqsMessage); + var replyTo = ReadReplyTo(sqsMessage); + var receiptHandle = ReadReceiptHandle(sqsMessage); + var partitionKey = ReadPartitionKey(sqsMessage); + var deduplicationId = ReadDeduplicationId(sqsMessage); + var subject = ReadSubject(sqsMessage); + + var bodyType = (contentType.Success ? contentType.Result : "plain/text"); + + var messageHeader = new MessageHeader( + messageId: messageId.Result ?? string.Empty, + topic: topic.Result ?? RoutingKey.Empty, + messageType.Result, + source: null, + type: string.Empty, + timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, + correlationId: correlationId.Success ? correlationId.Result : string.Empty, + replyTo: replyTo.Success ? new RoutingKey(replyTo.Result!) : RoutingKey.Empty, + contentType: bodyType!, + handledCount: handledCount.Result, + dataSchema: null, + subject: subject.Success ? subject.Result : string.Empty, + delayed: TimeSpan.Zero, + partitionKey: partitionKey.Success ? partitionKey.Result : string.Empty + ); + + message = new Message(messageHeader, ReadMessageBody(sqsMessage, bodyType!)); + + //deserialize the bag + var bag = ReadMessageBag(sqsMessage); + foreach (var key in bag.Keys) + { + message.Header.Bag.Add(key, bag[key]); + } + + if (deduplicationId.Success) + { + message.Header.Bag[HeaderNames.DeduplicationId] = deduplicationId.Result; + } + + if (receiptHandle.Success) + { + message.Header.Bag.Add("ReceiptHandle", receiptHandle.Result); + } + } + catch (Exception e) + { + s_logger.LogWarning(e, "Failed to create message from amqp message"); + message = FailureMessage(topic, messageId); + } + + return message; } - - internal class SqsMessageCreator : SqsMessageCreatorBase, ISqsMessageCreator + + private static MessageBody ReadMessageBody(Amazon.SQS.Model.Message sqsMessage, string contentType) { - private static readonly ILogger s_logger= ApplicationLogging.CreateLogger(); + if (contentType == CompressPayloadTransformerAsync.GZIP + || contentType == CompressPayloadTransformerAsync.DEFLATE + || contentType == CompressPayloadTransformerAsync.BROTLI) + return new MessageBody(sqsMessage.Body, contentType, CharacterEncoding.Base64); + + return new MessageBody(sqsMessage.Body, contentType); + } - public Message CreateMessage(Amazon.SQS.Model.Message sqsMessage) + private static Dictionary ReadMessageBag(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Bag, out MessageAttributeValue? value)) { - var topic = HeaderResult.Empty(); - var messageId = HeaderResult.Empty(); - var contentType = HeaderResult.Empty(); - var correlationId = HeaderResult.Empty(); - var handledCount = HeaderResult.Empty(); - var messageType = HeaderResult.Empty(); - var timeStamp = HeaderResult.Empty(); - var receiptHandle = HeaderResult.Empty(); - var replyTo = HeaderResult.Empty(); - - //TODO:CLOUD_EVENTS parse from headers - - Message message; try { - topic = ReadTopic(sqsMessage); - messageId = ReadMessageId(sqsMessage); - contentType = ReadContentType(sqsMessage); - correlationId = ReadCorrelationid(sqsMessage); - handledCount = ReadHandledCount(sqsMessage); - messageType = ReadMessageType(sqsMessage); - timeStamp = ReadTimestamp(sqsMessage); - replyTo = ReadReplyTo(sqsMessage); - receiptHandle = ReadReceiptHandle(sqsMessage); - - var bodyType = (contentType.Success ? contentType.Result : "plain/text"); - - var messageHeader = new MessageHeader( - messageId: messageId.Result ?? string.Empty, - topic: topic.Result ?? RoutingKey.Empty, - messageType.Result, - source: null, - type: string.Empty, - timeStamp: timeStamp.Success ? timeStamp.Result : DateTime.UtcNow, - correlationId: correlationId.Success ? correlationId.Result : string.Empty, - replyTo: replyTo.Success ? new RoutingKey(replyTo.Result!) : RoutingKey.Empty, - contentType: bodyType!, - handledCount: handledCount.Result, - dataSchema: null, - subject: null, - delayed: TimeSpan.Zero - ); - - message = new Message(messageHeader, ReadMessageBody(sqsMessage, bodyType!)); - - //deserialize the bag - var bag = ReadMessageBag(sqsMessage); - foreach (var key in bag.Keys) - { - message.Header.Bag.Add(key, bag[key]); - } - - if(receiptHandle.Success) - message.Header.Bag.Add("ReceiptHandle", receiptHandle.Result!); + var bag = JsonSerializer.Deserialize>(value.StringValue, + JsonSerialisationOptions.Options); + if (bag != null) + return bag; } - catch (Exception e) + catch (Exception) { - s_logger.LogWarning(e, "Failed to create message from amqp message"); - message = FailureMessage(topic, messageId); + //we weill just suppress conversion errors, and return an empty bag } - - return message; } - private static MessageBody ReadMessageBody(Amazon.SQS.Model.Message sqsMessage, string contentType) + return new Dictionary(); + } + + private static HeaderResult ReadReplyTo(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ReplyTo, out MessageAttributeValue? value)) { - if(contentType == CompressPayloadTransformerAsync.GZIP - || contentType == CompressPayloadTransformerAsync.DEFLATE - || contentType == CompressPayloadTransformerAsync.BROTLI) - return new MessageBody(sqsMessage.Body, contentType, CharacterEncoding.Base64); - - return new MessageBody(sqsMessage.Body, contentType); + return new HeaderResult(value.StringValue, true); } - private Dictionary ReadMessageBag(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(string.Empty, true); + } + + private static HeaderResult ReadTimestamp(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Timestamp, out MessageAttributeValue? value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Bag, out MessageAttributeValue? value)) + if (DateTime.TryParse(value.StringValue, out DateTime timestamp)) { - try - { - var bag = JsonSerializer.Deserialize>(value.StringValue, JsonSerialisationOptions.Options); - if (bag != null) - return bag; - } - catch (Exception) - { - //we weill just suppress conversion errors, and return an empty bag - } + return new HeaderResult(timestamp, true); } - return new Dictionary(); } - private HeaderResult ReadReplyTo(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(DateTime.UtcNow, true); + } + + private static HeaderResult ReadMessageType(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.MessageType, out MessageAttributeValue? value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ReplyTo, out MessageAttributeValue? value)) + if (Enum.TryParse(value.StringValue, out MessageType messageType)) { - return new HeaderResult(value.StringValue, true); + return new HeaderResult(messageType, true); } - return new HeaderResult(String.Empty, true); } - - private HeaderResult ReadTimestamp(Amazon.SQS.Model.Message sqsMessage) + + return new HeaderResult(MessageType.MT_EVENT, true); + } + + private static HeaderResult ReadHandledCount(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.HandledCount, out MessageAttributeValue? value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Timestamp, out MessageAttributeValue? value)) + if (int.TryParse(value.StringValue, out int handledCount)) { - if (DateTime.TryParse(value.StringValue, out DateTime timestamp)) - { - return new HeaderResult(timestamp, true); - } + return new HeaderResult(handledCount, true); } - return new HeaderResult(DateTime.UtcNow, true); } - private HeaderResult ReadMessageType(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(0, true); + } + + private static HeaderResult ReadCorrelationId(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.CorrelationId, + out MessageAttributeValue? correlationId)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.MessageType, out MessageAttributeValue? value)) - { - if (Enum.TryParse(value.StringValue, out MessageType messageType)) - { - return new HeaderResult(messageType, true); - } - } - return new HeaderResult(MessageType.MT_EVENT, true); + return new HeaderResult(correlationId.StringValue, true); + } + + return new HeaderResult(string.Empty, true); + } + + private static HeaderResult ReadContentType(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ContentType, out MessageAttributeValue? value)) + { + return new HeaderResult(value.StringValue, true); } - private HeaderResult ReadHandledCount(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(string.Empty, true); + } + + private static HeaderResult ReadMessageId(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Id, out MessageAttributeValue? value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.HandledCount, out MessageAttributeValue? value)) - { - if (int.TryParse(value.StringValue, out int handledCount)) - { - return new HeaderResult(handledCount, true); - } - } - return new HeaderResult(0, true); + return new HeaderResult(value.StringValue, true); } - private HeaderResult ReadCorrelationid(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(string.Empty, true); + } + + private static HeaderResult ReadTopic(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Topic, out MessageAttributeValue? value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.CorrelationId, out MessageAttributeValue? correlationId)) + //we have an arn, and we want the topic + var topic = value.StringValue ?? string.Empty; + if (Arn.TryParse(topic, out var arn)) { - return new HeaderResult(correlationId.StringValue, true); + return new HeaderResult(new RoutingKey(arn.Resource), true); } - return new HeaderResult(string.Empty, true); - } - private HeaderResult ReadContentType(Amazon.SQS.Model.Message sqsMessage) - { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.ContentType, out MessageAttributeValue? value)) + var indexOf = topic.LastIndexOf('/'); + if (indexOf != -1) { - return new HeaderResult(value.StringValue, true); + return new HeaderResult(new RoutingKey(topic.Substring(indexOf + 1)), true); } - return new HeaderResult(string.Empty, true); + + return new HeaderResult(new RoutingKey(topic), true); } - private HeaderResult ReadMessageId(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(RoutingKey.Empty, true); + } + + private static HeaderResult ReadPartitionKey(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageGroupId, out var value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Id, out MessageAttributeValue? value)) - { - return new HeaderResult(value.StringValue, true); - } - return new HeaderResult(string.Empty, true); + //we have an arn, and we want the topic + var messageGroupId = value; + return new HeaderResult(messageGroupId, true); + } + + return new HeaderResult(null, false); + } + + private static HeaderResult ReadDeduplicationId(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageDeduplicationId, out var value)) + { + //we have an arn, and we want the topic + var messageGroupId = value; + return new HeaderResult(messageGroupId, true); } - private HeaderResult ReadTopic(Amazon.SQS.Model.Message sqsMessage) + return new HeaderResult(null, false); + } + + private static HeaderResult ReadSubject(Amazon.SQS.Model.Message sqsMessage) + { + if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Subject, out var value)) { - if (sqsMessage.MessageAttributes.TryGetValue(HeaderNames.Topic, out MessageAttributeValue? value)) - { - //we have an arn, and we want the topic - var arnElements = value.StringValue.Split(':'); - var topic = arnElements[(int)ARNAmazonSNS.TopicName]; - return new HeaderResult(new RoutingKey(topic), true); - } - return new HeaderResult(RoutingKey.Empty, true); + //we have an arn, and we want the topic + var subject = value.StringValue; + return new HeaderResult(subject, true); } + + return new HeaderResult(null, false); } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs index b4a14fc6d2..63bee0c50d 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducer.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,6 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #endregion using System; @@ -28,135 +30,136 @@ THE SOFTWARE. */ using Microsoft.Extensions.Logging; using Paramore.Brighter.Tasks; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The SQS Message producer +/// +public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerAsync, IAmAMessageProducerSync { + private readonly SqsPublication _publication; + private readonly AWSClientFactory _clientFactory; + + /// + /// The publication configuration for this producer + /// + public Publication Publication => _publication; + + /// + /// The OTel Span we are writing Producer events too + /// + public Activity? Span { get; set; } + + /// + /// Initialize a new instance of the . + /// + /// How do we connect to AWS in order to manage middleware + /// Configuration of a producer + public SqsMessageProducer(AWSMessagingGatewayConnection connection, SqsPublication publication) + : base(connection) + { + _publication = publication; + _clientFactory = new AWSClientFactory(connection); + + if (publication.QueueUrl != null) + { + ChannelQueueUrl = publication.QueueUrl; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public ValueTask DisposeAsync() => new(); + /// - /// Class SqsMessageProducer. + /// Confirm the queue exists. /// - public class SqsMessageProducer : AWSMessagingGateway, IAmAMessageProducerSync, IAmAMessageProducerAsync + /// The queue name. + public bool ConfirmQueueExists(string? queue = null) + => BrighterAsyncContext.Run(async () => await ConfirmQueueExistsAsync(queue)); + + /// + /// Confirm the queue exists. + /// + /// The queue name. + /// The . + /// Return true if the queue exists otherwise return false + public async Task ConfirmQueueExistsAsync(string? queue = null, CancellationToken cancellationToken = default) { - private readonly SnsPublication _publication; - private readonly AWSClientFactory _clientFactory; - - /// - /// The publication configuration for this producer - /// - public Publication Publication { get { return _publication; } } - - /// - /// The OTel Span we are writing Producer events too - /// - public Activity? Span { get; set; } - /// - /// Initializes a new instance of the class. - /// - /// How do we connect to AWS in order to manage middleware - /// Configuration of a producer - public SqsMessageProducer(AWSMessagingGatewayConnection connection, SnsPublication publication) - : base(connection) + //Only do this on first send for a topic for efficiency; won't auto-recreate when goes missing at runtime as a result + if (!string.IsNullOrEmpty(ChannelQueueUrl)) { - _publication = publication; - _clientFactory = new AWSClientFactory(connection); + return true; + } + + _publication.SqsAttributes ??= new SqsAttributes(); - if (publication.TopicArn != null) - ChannelTopicArn = publication.TopicArn; + // For SQS Publish, it should be always Point-to-Point + _publication.SqsAttributes.ChannelType = ChannelType.PointToPoint; + RoutingKey? routingKey = null; + if (queue is not null) + { + routingKey = new RoutingKey(queue); } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() { } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public ValueTask DisposeAsync() + else if (_publication.Topic is not null) { - return new ValueTask(Task.CompletedTask); + routingKey = _publication.Topic; } - - public bool ConfirmTopicExists(string? topic = null) => BrighterAsyncContext.Run(async () => await ConfirmTopicExistsAsync(topic)); - - public async Task ConfirmTopicExistsAsync(string? topic = null, CancellationToken cancellationToken = default) - { - //Only do this on first send for a topic for efficiency; won't auto-recreate when goes missing at runtime as a result - if (!string.IsNullOrEmpty(ChannelTopicArn)) return !string.IsNullOrEmpty(ChannelTopicArn); - - RoutingKey? routingKey = null; - if (topic is null && _publication.Topic is not null) - routingKey = _publication.Topic; - else if (topic is not null) - routingKey = new RoutingKey(topic); - - if (routingKey is null) - throw new ConfigurationException("No topic specified for producer"); - - await EnsureTopicAsync( - routingKey, - _publication.FindTopicBy, - _publication.SnsAttributes, _publication.MakeChannels, cancellationToken); - - return !string.IsNullOrEmpty(ChannelTopicArn); - } - - /// - /// Sends the specified message. - /// - /// The message. - /// Allows cancellation of the Send operation - public async Task SendAsync(Message message, CancellationToken cancellationToken = default) - { - s_logger.LogDebug("SQSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", - message.Header.Topic, message.Id, message.Body); - - await ConfirmTopicExistsAsync(message.Header.Topic, cancellationToken); - - if (string.IsNullOrEmpty(ChannelTopicArn)) - throw new InvalidOperationException($"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body} as the topic does not exist"); - - using var client = _clientFactory.CreateSnsClient(); - var publisher = new SqsMessagePublisher(ChannelTopicArn!, client); - var messageId = await publisher.PublishAsync(message); - - if (messageId == null) - throw new InvalidOperationException($"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}"); - - s_logger.LogDebug( - "SQSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", - message.Header.Topic, message.Id, messageId); - } - - /// - /// Sends the specified message. - /// Sync over Async - /// - /// The message. - public void Send(Message message) => BrighterAsyncContext.Run(async () => await SendAsync(message)); - - /// - /// Sends the specified message, with a delay. - /// - /// The message. - /// The sending delay - /// Task. - public void SendWithDelay(Message message, TimeSpan? delay= null) + + if (RoutingKey.IsNullOrEmpty(routingKey)) { - //TODO: Delay should set a visibility timeout - Send(message); + throw new ConfigurationException("No topic specified for producer"); } - /// - /// Sends the specified message, with a delay - /// - /// The message - /// The sending delay - /// Cancels the send operation - /// - public async Task SendWithDelayAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken = default) + var queueUrl = await EnsureQueueAsync( + routingKey, + _publication.FindQueueBy, + _publication.SqsAttributes, + _publication.MakeChannels, + cancellationToken); + + return !string.IsNullOrEmpty(queueUrl); + } + + public async Task SendAsync(Message message, CancellationToken cancellationToken = default) + => await SendWithDelayAsync(message, null, cancellationToken); + + public async Task SendWithDelayAsync(Message message, TimeSpan? delay, + CancellationToken cancellationToken = default) + { + s_logger.LogDebug( + "SQSMessageProducer: Publishing message with topic {Topic} and id {Id} and message: {Request}", + message.Header.Topic, message.Id, message.Body); + + await ConfirmQueueExistsAsync(message.Header.Topic, cancellationToken); + + using var client = _clientFactory.CreateSqsClient(); + var type = _publication.SqsAttributes?.Type ?? SnsSqsType.Standard; + var sender = new SqsMessageSender(ChannelQueueUrl!, type, client); + var messageId = await sender.SendAsync(message, delay, cancellationToken); + + if (messageId == null) { - //TODO: Delay should set the visibility timeout - await SendAsync(message, cancellationToken); + throw new InvalidOperationException( + $"Failed to publish message with topic {message.Header.Topic} and id {message.Id} and message: {message.Body}"); } + + s_logger.LogDebug( + "SQSMessageProducer: Published message with topic {Topic}, Brighter messageId {MessageId} and SNS messageId {SNSMessageId}", + message.Header.Topic, message.Id, messageId); } + + public void Send(Message message) => SendWithDelay(message, null); + + public void SendWithDelay(Message message, TimeSpan? delay) + => BrighterAsyncContext.Run(async () => await SendWithDelayAsync(message, delay)); } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducerFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducerFactory.cs new file mode 100644 index 0000000000..33b38531f0 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageProducerFactory.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The factory +/// +public class SqsMessageProducerFactory : IAmAMessageProducerFactory +{ + private readonly AWSMessagingGatewayConnection _connection; + private readonly IEnumerable _publications; + + /// + /// Initialize new instance of . + /// + /// The . + /// The collection of . + public SqsMessageProducerFactory(AWSMessagingGatewayConnection connection, + IEnumerable publications) + { + _connection = connection; + _publications = publications; + } + + /// + /// + /// Sync over async used here, alright in the context of producer creation + /// + public Dictionary Create() + { + var producers = new Dictionary(); + foreach (var sqs in _publications) + { + if (sqs.Topic is null) + { + throw new ConfigurationException("Missing topic on Publication"); + } + + var producer = new SqsMessageProducer(_connection, sqs); + if (producer.ConfirmQueueExists()) + { + producers[sqs.Topic] = producer; + } + else + { + throw new ConfigurationException($"Missing SQS queue: {sqs.Topic}"); + } + } + + return producers; + } + + /// + public async Task> CreateAsync() + { + var producers = new Dictionary(); + foreach (var sqs in _publications) + { + if (sqs.Topic is null) + { + throw new ConfigurationException("Missing topic on Publication"); + } + + var producer = new SqsMessageProducer(_connection, sqs); + if (await producer.ConfirmQueueExistsAsync()) + { + producers[sqs.Topic] = producer; + } + else + { + throw new ConfigurationException($"Missing SQS queue: {sqs.Topic}"); + } + } + + return producers; + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs deleted file mode 100644 index a9f171727e..0000000000 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessagePublisher.cs +++ /dev/null @@ -1,75 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2022 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ -#endregion - -using System; -using System.Collections.Generic; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon.SimpleNotificationService; -using Amazon.SimpleNotificationService.Model; - -namespace Paramore.Brighter.MessagingGateway.AWSSQS -{ - public class SqsMessagePublisher - { - private readonly string _topicArn; - private readonly AmazonSimpleNotificationServiceClient _client; - - public SqsMessagePublisher(string topicArn, AmazonSimpleNotificationServiceClient client) - { - _topicArn = topicArn; - _client = client; - } - - public async Task PublishAsync(Message message) - { - var messageString = message.Body.Value; - var publishRequest = new PublishRequest(_topicArn, messageString, message.Header.Subject); - - var messageAttributes = new Dictionary(); - messageAttributes.Add(HeaderNames.Id, new MessageAttributeValue{StringValue = Convert.ToString(message.Header.MessageId), DataType = "String"}); - messageAttributes.Add(HeaderNames.Topic, new MessageAttributeValue{StringValue = _topicArn, DataType = "String"}); - messageAttributes.Add(HeaderNames.ContentType, new MessageAttributeValue {StringValue = message.Header.ContentType, DataType = "String"}); - messageAttributes.Add(HeaderNames.CorrelationId, new MessageAttributeValue{StringValue = Convert.ToString(message.Header.CorrelationId), DataType = "String"}); - messageAttributes.Add(HeaderNames.HandledCount, new MessageAttributeValue {StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String"}); - messageAttributes.Add(HeaderNames.MessageType, new MessageAttributeValue{StringValue = message.Header.MessageType.ToString(), DataType = "String"}); - messageAttributes.Add(HeaderNames.Timestamp, new MessageAttributeValue{StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String"}); - if (!string.IsNullOrEmpty(message.Header.ReplyTo)) - messageAttributes.Add(HeaderNames.ReplyTo, new MessageAttributeValue{StringValue = Convert.ToString(message.Header.ReplyTo), DataType = "String"}); - - //we can set up to 10 attributes; we have set 6 above, so use a single JSON object as the bag - var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); - - messageAttributes.Add(HeaderNames.Bag, new MessageAttributeValue{StringValue = Convert.ToString(bagJson), DataType = "String"}); - publishRequest.MessageAttributes = messageAttributes; - - var response = await _client.PublishAsync(publishRequest); - if (response.HttpStatusCode == System.Net.HttpStatusCode.OK || response.HttpStatusCode == System.Net.HttpStatusCode.Created || response.HttpStatusCode == System.Net.HttpStatusCode.Accepted) - { - return response.MessageId; - } - - return null; - } - } -} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageSender.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageSender.cs new file mode 100644 index 0000000000..028d2b17e3 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsMessageSender.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// Class responsible for sending a message to a SQS +/// +public class SqsMessageSender +{ + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly TimeSpan s_maxDelay = TimeSpan.FromSeconds(900); + + private readonly string _queueUrl; + private readonly SnsSqsType _queueType; + private readonly AmazonSQSClient _client; + + /// + /// Initialize the + /// + /// The queue ARN + /// The queue type + /// The SQS Client + public SqsMessageSender(string queueUrl, SnsSqsType queueType, AmazonSQSClient client) + { + _queueUrl = queueUrl; + _queueType = queueType; + _client = client; + } + + /// + /// Sending message via SQS + /// + /// The message. + /// The delay in ms. 0 is no delay. Defaults to 0 + /// A that cancels the Publish operation + /// The message id. + public async Task SendAsync(Message message, TimeSpan? delay, CancellationToken cancellationToken) + { + var request = new SendMessageRequest + { + QueueUrl = _queueUrl, + MessageBody = message.Body.Value + }; + + delay ??= TimeSpan.Zero; + if (delay > TimeSpan.Zero) + { + // SQS has a hard limit of 15min for Delay in Seconds + if (delay.Value > s_maxDelay) + { + delay = s_maxDelay; + s_logger.LogWarning("Set delay from {CurrentDelay} to 15min (SQS support up to 15min)", delay); + } + + request.DelaySeconds = (int)delay.Value.TotalSeconds; + } + + if (_queueType == SnsSqsType.Fifo) + { + request.MessageGroupId = message.Header.PartitionKey; + if (message.Header.Bag.TryGetValue(HeaderNames.DeduplicationId, out var deduplicationId)) + { + request.MessageDeduplicationId = (string)deduplicationId; + } + } + + var messageAttributes = new Dictionary + { + [HeaderNames.Id] = + new() { StringValue = message.Header.MessageId, DataType = "String" }, + [HeaderNames.Topic] = new() { StringValue = _queueUrl, DataType = "String" }, + [HeaderNames.ContentType] = new() { StringValue = message.Header.ContentType, DataType = "String" }, + [HeaderNames.CorrelationId] = + new() { StringValue = message.Header.CorrelationId, DataType = "String" }, + [HeaderNames.HandledCount] = + new() { StringValue = Convert.ToString(message.Header.HandledCount), DataType = "String" }, + [HeaderNames.MessageType] = + new() { StringValue = message.Header.MessageType.ToString(), DataType = "String" }, + [HeaderNames.Timestamp] = new() + { + StringValue = Convert.ToString(message.Header.TimeStamp), DataType = "String" + } + }; + + if (!string.IsNullOrEmpty(message.Header.ReplyTo)) + { + messageAttributes.Add(HeaderNames.ReplyTo, + new MessageAttributeValue { StringValue = message.Header.ReplyTo, DataType = "String" }); + } + + if (!string.IsNullOrEmpty(message.Header.Subject)) + { + messageAttributes.Add(HeaderNames.Subject, + new MessageAttributeValue { StringValue = message.Header.Subject, DataType = "String" }); + } + + // we can set up to 10 attributes; we have set 6 above, so use a single JSON object as the bag + var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); + messageAttributes[HeaderNames.Bag] = new() { StringValue = bagJson, DataType = "String" }; + request.MessageAttributes = messageAttributes; + + var response = await _client.SendMessageAsync(request, cancellationToken); + if (response.HttpStatusCode is HttpStatusCode.OK or HttpStatusCode.Created + or HttpStatusCode.Accepted) + { + return response.MessageId; + } + + return null; + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsProducerRegistryFactory.cs new file mode 100644 index 0000000000..eb3bc3c26f --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsProducerRegistryFactory.cs @@ -0,0 +1,73 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2022 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The SQS Message Producer registry factory +/// +public class SqsProducerRegistryFactory : IAmAProducerRegistryFactory +{ + private readonly AWSMessagingGatewayConnection _connection; + private readonly IEnumerable _sqsPublications; + + /// + /// Create a collection of producers from the publication information + /// + /// The Connection to use to connect to AWS + /// The publication describing the SNS topic that we want to use + public SqsProducerRegistryFactory( + AWSMessagingGatewayConnection connection, + IEnumerable sqsPublications) + { + _connection = connection; + _sqsPublications = sqsPublications; + } + + /// + /// Create a message producer for each publication, add it into the registry under the key of the topic + /// + /// The with . + public IAmAProducerRegistry Create() + { + var producerFactory = new SqsMessageProducerFactory(_connection, _sqsPublications); + return new ProducerRegistry(producerFactory.Create()); + } + + /// + /// Create a message producer for each publication, add it into the registry under the key of the topic + /// + /// The . + /// The with . + public async Task CreateAsync(CancellationToken ct = default) + { + var producerFactory = new SqsMessageProducerFactory(_connection, _sqsPublications); + return new ProducerRegistry(await producerFactory.CreateAsync()); + } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsPublication.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsPublication.cs new file mode 100644 index 0000000000..3e13f6787a --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsPublication.cs @@ -0,0 +1,26 @@ +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The SQS Message publication +/// +public class SqsPublication : Publication +{ + /// + /// Indicates how we should treat the routing key + /// QueueFindBy.Url -> the routing key is an url + /// TopicFindBy.Name -> Treat the routing key as a name & use GetQueueUrl to find it + /// + public QueueFindBy FindQueueBy { get; set; } = QueueFindBy.Name; + + /// + /// The attributes of the topic. If TopicARNs is set we will always assume that we do not + /// need to create or validate the SNS Topic + /// + public SqsAttributes? SqsAttributes { get; set; } + + /// + /// If we want to use queue Url and not queues you need to supply the Url to use for any message that you send to us, + /// as we use the topic from the header to dispatch to an url. + /// + public string? QueueUrl { get; set; } +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs index e179c30da7..bd7775bbda 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/SqsSubscription.cs @@ -26,199 +26,268 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; -namespace Paramore.Brighter.MessagingGateway.AWSSQS +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// A subscription for an SQS Consumer. +/// We will create infrastructure on the basis of Make Channels +/// Create = topic using routing key name, queue using channel name +/// Validate = look for topic using routing key name, queue using channel name +/// Assume = Assume Routing Key is Topic ARN, queue exists via channel name +/// +public class SqsSubscription : Subscription { /// - /// A subscription for an SQS Consumer. - /// We will create infrastructure on the basis of Make Channels - /// Create = topic using routing key name, queue using channel name - /// Validate = look for topic using routing key name, queue using channel name - /// Assume = Assume Routing Key is Topic ARN, queue exists via channel name + /// This governs how long, in seconds, a 'lock' is held on a message for one consumer + /// to process. SQS calls this the VisibilityTimeout + /// + public int LockTimeout { get; } + + /// + /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. + /// + public int DelaySeconds { get; } + + /// + /// The length of time, in seconds, for which Amazon SQS retains a message + /// + public int MessageRetentionPeriod { get; } + + /// + /// The routing key type. + /// + public ChannelType ChannelType { get; } + + /// + /// Indicates how we should treat the routing key + /// QueueFindBy.Url -> the routing key is an URL + /// TopicFindBy.Name -> Treat the routing key as a name & use GetQueueUrl to find it + /// + public QueueFindBy FindQueueBy { get; } + + /// + /// Indicates how we should treat the routing key + /// TopicFindBy.Arn -> the routing key is an Arn + /// TopicFindBy.Convention -> The routing key is a name, but use convention to make an Arn for this account + /// TopicFindBy.Name -> Treat the routing key as a name & use ListTopics to find it (rate limited 30/s) + /// + public TopicFindBy FindTopicBy { get; } + + /// + /// The attributes of the topic. If TopicARN is set we will always assume that we do not + /// need to create or validate the SNS Topic + /// + public SnsAttributes? SnsAttributes { get; } + + /// + /// The JSON serialization of the queue's access control policy. + /// + public string? IAMPolicy { get; } + + /// + /// Indicate that the Raw Message Delivery setting is enabled or disabled /// - public class SqsSubscription : Subscription + public bool RawMessageDelivery { get; } + + /// + /// The policy that controls when we send messages to a DLQ after too many requeue attempts + /// + public RedrivePolicy? RedrivePolicy { get; } + + + /// + /// A list of resource tags to use when creating the queue + /// + public Dictionary? Tags { get; } + + /// + /// The AWS SQS type. + /// + public SnsSqsType SqsType { get; } + + /// + /// Enables or disable content-based deduplication, for Fifo queues. + /// + public bool ContentBasedDeduplication { get; } + + /// + /// Specifies whether message deduplication occurs at the message group or queue level. + /// This configuration is used for high throughput for FIFO queues configuration + /// + public DeduplicationScope? DeduplicationScope { get; } + + /// + /// Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group + /// This configuration is used for high throughput for FIFO queues configuration + /// + public int? FifoThroughputLimit { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Type of the data. + /// The name. Defaults to the data type's full name. + /// The channel name. Defaults to the data type's full name. + /// The routing key. Defaults to the data type's full name. + /// The no of threads reading this channel. + /// The number of messages to buffer at any one time, also the number of messages to retrieve at once. Min of 1 Max of 10 + /// The timeout in milliseconds. + /// The number of times you want to requeue a message before dropping it. + /// The number of milliseconds to delay the delivery of a requeue message for. + /// The number of unacceptable messages to handle, before stopping reading from the channel. + /// Is this channel read asynchronously + /// The channel factory to create channels for Consumer. + /// What is the visibility timeout for the queue + /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. + /// The length of time, in seconds, for which Amazon SQS retains a message + /// Is the Topic an Arn, should be treated as an Arn by convention, or a name + /// The queue's policy. A valid AWS policy. + /// The policy that controls when and where requeued messages are sent to the DLQ + /// The attributes of the Topic, either ARN if created, or attributes for creation + /// Resource tags to be added to the queue + /// Should we make channels if they don't exist, defaults to creating + /// The indication of Raw Message Delivery setting is enabled or disabled + /// How long to pause when a channel is empty in milliseconds + /// How long to pause when there is a channel failure in milliseconds + /// The SQS Type + /// Enables or disable content-based deduplication + /// Specifies whether message deduplication occurs at the message group or queue level + /// Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group + /// Specifies the routing key type + /// How the queue should be found when is point-to-point. + public SqsSubscription( + Type dataType, + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, + int bufferSize = 1, + int noOfPerformers = 1, + TimeSpan? timeOut = null, + int requeueCount = -1, + TimeSpan? requeueDelay = null, + int unacceptableMessageLimit = 0, + MessagePumpType messagePumpType = MessagePumpType.Unknown, + IAmAChannelFactory? channelFactory = null, + int lockTimeout = 10, + int delaySeconds = 0, + int messageRetentionPeriod = 345600, + TopicFindBy findTopicBy = TopicFindBy.Name, + string? iAmPolicy = null, + RedrivePolicy? redrivePolicy = null, + SnsAttributes? snsAttributes = null, + Dictionary? tags = null, + OnMissingChannel makeChannels = OnMissingChannel.Create, + bool rawMessageDelivery = true, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null, + SnsSqsType sqsType = SnsSqsType.Standard, + bool contentBasedDeduplication = true, + DeduplicationScope? deduplicationScope = null, + int? fifoThroughputLimit = null, + ChannelType channelType = ChannelType.PubSub, + QueueFindBy findQueueBy = QueueFindBy.Name + ) + : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, + requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, + channelFailureDelay) { - /// - /// This governs how long, in seconds, a 'lock' is held on a message for one consumer - /// to process. SQS calls this the VisibilityTimeout - /// - public int LockTimeout { get; } - - /// - /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. - /// - public int DelaySeconds { get; } - - /// - /// The length of time, in seconds, for which Amazon SQS retains a message - /// - public int MessageRetentionPeriod { get; } - - /// - /// Indicates how we should treat the routing key - /// TopicFindBy.Arn -> the routing key is an Arn - /// TopicFindBy.Convention -> The routing key is a name, but use convention to make an Arn for this account - /// TopicFindBy.Name -> Treat the routing key as a name & use ListTopics to find it (rate limited 30/s) - /// - public TopicFindBy FindTopicBy { get; } - - /// - /// The JSON serialization of the queue's access control policy. - /// - public string? IAMPolicy { get; } - - /// - /// Indicate that the Raw Message Delivery setting is enabled or disabled - /// - public bool RawMessageDelivery { get; } - - /// - /// The policy that controls when we send messages to a DLQ after too many requeue attempts - /// - public RedrivePolicy? RedrivePolicy { get; } - - /// - /// The attributes of the topic. If TopicARN is set we will always assume that we do not - /// need to create or validate the SNS Topic - /// - public SnsAttributes? SnsAttributes { get; } - - /// - /// A list of resource tags to use when creating the queue - /// - public Dictionary? Tags { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Type of the data. - /// The name. Defaults to the data type's full name. - /// The channel name. Defaults to the data type's full name. - /// The routing key. Defaults to the data type's full name. - /// The no of threads reading this channel. - /// The number of messages to buffer at any one time, also the number of messages to retrieve at once. Min of 1 Max of 10 - /// The timeout in milliseconds. - /// The number of times you want to requeue a message before dropping it. - /// The number of milliseconds to delay the delivery of a requeue message for. - /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously - /// The channel factory to create channels for Consumer. - /// What is the visibility timeout for the queue - /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. - /// The length of time, in seconds, for which Amazon SQS retains a message - /// Is the Topic an Arn, should be treated as an Arn by convention, or a name - /// The queue's policy. A valid AWS policy. - /// The policy that controls when and where requeued messages are sent to the DLQ - /// The attributes of the Topic, either ARN if created, or attributes for creation - /// Resource tags to be added to the queue - /// Should we make channels if they don't exist, defaults to creating - /// The indication of Raw Message Delivery setting is enabled or disabled - /// How long to pause when a channel is empty in milliseconds - /// How long to pause when there is a channel failure in milliseconds - public SqsSubscription( - Type dataType, - SubscriptionName? name = null, - ChannelName? channelName = null, - RoutingKey? routingKey = null, - int bufferSize = 1, - int noOfPerformers = 1, - TimeSpan? timeOut = null, - int requeueCount = -1, - TimeSpan? requeueDelay = null, - int unacceptableMessageLimit = 0, - MessagePumpType messagePumpType = MessagePumpType.Unknown, - IAmAChannelFactory? channelFactory = null, - int lockTimeout = 10, - int delaySeconds = 0, - int messageRetentionPeriod = 345600, - TopicFindBy findTopicBy = TopicFindBy.Name, - string? iAmPolicy = null, - RedrivePolicy? redrivePolicy = null, - SnsAttributes? snsAttributes = null, - Dictionary? tags = null, - OnMissingChannel makeChannels = OnMissingChannel.Create, - bool rawMessageDelivery = true, - TimeSpan? emptyChannelDelay = null, - TimeSpan? channelFailureDelay = null - ) - : base(dataType, name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, - requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay) - { - LockTimeout = lockTimeout; - DelaySeconds = delaySeconds; - MessageRetentionPeriod = messageRetentionPeriod; - FindTopicBy = findTopicBy; - IAMPolicy = iAmPolicy; - RawMessageDelivery = rawMessageDelivery; - RedrivePolicy = redrivePolicy; - SnsAttributes = snsAttributes; - Tags = tags; - } + LockTimeout = lockTimeout; + DelaySeconds = delaySeconds; + MessageRetentionPeriod = messageRetentionPeriod; + FindTopicBy = findTopicBy; + IAMPolicy = iAmPolicy; + RawMessageDelivery = rawMessageDelivery; + RedrivePolicy = redrivePolicy; + SnsAttributes = snsAttributes; + Tags = tags; + SqsType = sqsType; + ContentBasedDeduplication = contentBasedDeduplication; + DeduplicationScope = deduplicationScope; + FifoThroughputLimit = fifoThroughputLimit; + ChannelType = channelType; + FindQueueBy = findQueueBy; } +} +/// +/// A subscription for an SQS Consumer. +/// We will create infrastructure on the basis of Make Channels +/// Create = topic using routing key name, queue using channel name +/// Validate = look for topic using routing key name, queue using channel name +/// Assume = Assume Routing Key is Topic ARN, queue exists via channel name +/// +public class SqsSubscription : SqsSubscription where T : IRequest +{ /// - /// A subscription for an SQS Consumer. - /// We will create infrastructure on the basis of Make Channels - /// Create = topic using routing key name, queue using channel name - /// Validate = look for topic using routing key name, queue using channel name - /// Assume = Assume Routing Key is Topic ARN, queue exists via channel name + /// Initializes a new instance of the class. /// - public class SqsSubscription : SqsSubscription where T : IRequest + /// The name. Defaults to the data type's full name. + /// The channel name. Defaults to the data type's full name. + /// The routing key. Defaults to the data type's full name. + /// The no of threads reading this channel. + /// The number of messages to buffer at any one time, also the number of messages to retrieve at once. Min of 1 Max of 10 + /// The timeout. Defaults to 300 milliseconds. + /// The number of times you want to requeue a message before dropping it. + /// The number of milliseconds to delay the delivery of a requeue message for. + /// The number of unacceptable messages to handle, before stopping reading from the channel. + /// Is this channel read asynchronously + /// The channel factory to create channels for Consumer. + /// What is the visibility timeout for the queue + /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. + /// The length of time, in seconds, for which Amazon SQS retains a message + /// Is the Topic an Arn, should be treated as an Arn by convention, or a name + /// The queue's policy. A valid AWS policy. + /// The policy that controls when and where requeued messages are sent to the DLQ + /// The attributes of the Topic, either ARN if created, or attributes for creation + /// Resource tags to be added to the queue + /// Should we make channels if they don't exist, defaults to creating + /// The indication of Raw Message Delivery setting is enabled or disabled + /// How long to pause when a channel is empty in milliseconds + /// How long to pause when there is a channel failure in milliseconds + /// The SQS Type + /// Enables or disable content-based deduplication + /// Specifies whether message deduplication occurs at the message group or queue level + /// Specifies whether the FIFO queue throughput quota applies to the entire queue or per message group + /// Specifies the routing key type + /// How the queue should be found when is point-to-point. + public SqsSubscription( + SubscriptionName? name = null, + ChannelName? channelName = null, + RoutingKey? routingKey = null, + int bufferSize = 1, + int noOfPerformers = 1, + TimeSpan? timeOut = null, + int requeueCount = -1, + TimeSpan? requeueDelay = null, + int unacceptableMessageLimit = 0, + MessagePumpType messagePumpType = MessagePumpType.Proactor, + IAmAChannelFactory? channelFactory = null, + int lockTimeout = 10, + int delaySeconds = 0, + int messageRetentionPeriod = 345600, + TopicFindBy findTopicBy = TopicFindBy.Name, + string? iAmPolicy = null, + RedrivePolicy? redrivePolicy = null, + SnsAttributes? snsAttributes = null, + Dictionary? tags = null, + OnMissingChannel makeChannels = OnMissingChannel.Create, + bool rawMessageDelivery = true, + TimeSpan? emptyChannelDelay = null, + TimeSpan? channelFailureDelay = null, + SnsSqsType sqsType = SnsSqsType.Standard, + bool contentBasedDeduplication = true, + DeduplicationScope? deduplicationScope = null, + int? fifoThroughputLimit = null, + ChannelType channelType = ChannelType.PubSub, + QueueFindBy findQueueBy = QueueFindBy.Name + ) + : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, + requeueDelay, + unacceptableMessageLimit, messagePumpType, channelFactory, lockTimeout, delaySeconds, + messageRetentionPeriod, findTopicBy, + iAmPolicy, redrivePolicy, snsAttributes, tags, makeChannels, rawMessageDelivery, emptyChannelDelay, + channelFailureDelay, sqsType, contentBasedDeduplication, deduplicationScope, fifoThroughputLimit, + channelType, findQueueBy) { - /// - /// Initializes a new instance of the class. - /// - /// The name. Defaults to the data type's full name. - /// The channel name. Defaults to the data type's full name. - /// The routing key. Defaults to the data type's full name. - /// The no of threads reading this channel. - /// The number of messages to buffer at any one time, also the number of messages to retrieve at once. Min of 1 Max of 10 - /// The timeout. Defaults to 300 milliseconds. - /// The number of times you want to requeue a message before dropping it. - /// The number of milliseconds to delay the delivery of a requeue message for. - /// The number of unacceptable messages to handle, before stopping reading from the channel. - /// Is this channel read asynchronously - /// The channel factory to create channels for Consumer. - /// What is the visibility timeout for the queue - /// The length of time, in seconds, for which the delivery of all messages in the queue is delayed. - /// The length of time, in seconds, for which Amazon SQS retains a message - /// Is the Topic an Arn, should be treated as an Arn by convention, or a name - /// The queue's policy. A valid AWS policy. - /// The policy that controls when and where requeued messages are sent to the DLQ - /// The attributes of the Topic, either ARN if created, or attributes for creation - /// Resource tags to be added to the queue - /// Should we make channels if they don't exist, defaults to creating - /// The indication of Raw Message Delivery setting is enabled or disabled - /// How long to pause when a channel is empty in milliseconds - /// How long to pause when there is a channel failure in milliseconds - public SqsSubscription( - SubscriptionName? name = null, - ChannelName? channelName = null, - RoutingKey? routingKey = null, - int bufferSize = 1, - int noOfPerformers = 1, - TimeSpan? timeOut = null, - int requeueCount = -1, - TimeSpan? requeueDelay = null, - int unacceptableMessageLimit = 0, - MessagePumpType messagePumpType = MessagePumpType.Proactor, - IAmAChannelFactory? channelFactory = null, - int lockTimeout = 10, - int delaySeconds = 0, - int messageRetentionPeriod = 345600, - TopicFindBy findTopicBy = TopicFindBy.Name, - string? iAmPolicy = null, - RedrivePolicy? redrivePolicy = null, - SnsAttributes? snsAttributes = null, - Dictionary? tags = null, - OnMissingChannel makeChannels = OnMissingChannel.Create, - bool rawMessageDelivery = true, - TimeSpan? emptyChannelDelay = null, - TimeSpan? channelFailureDelay = null - ) - : base(typeof(T), name, channelName, routingKey, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, - unacceptableMessageLimit, messagePumpType, channelFactory, lockTimeout, delaySeconds, messageRetentionPeriod,findTopicBy, - iAmPolicy,redrivePolicy, snsAttributes, tags, makeChannels, rawMessageDelivery, emptyChannelDelay, channelFailureDelay) - { - } } } diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByName.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByName.cs new file mode 100644 index 0000000000..05163de636 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByName.cs @@ -0,0 +1,46 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The class is responsible for validating an AWS SQS queue by its name. +/// +public class ValidateQueueByName : IValidateQueue, IDisposable +{ + private readonly AmazonSQSClient _client; + private readonly SnsSqsType _type; + + /// + /// Initialize new instance of . + /// + /// The client. + /// The SQS type. + public ValidateQueueByName(AmazonSQSClient client, SnsSqsType type) + { + _client = client; + _type = type; + } + + /// + public async Task<(bool, string?)> ValidateAsync(string queue, CancellationToken cancellationToken = default) + { + try + { + queue = queue.ToValidSQSQueueName(_type == SnsSqsType.Fifo); + var queueUrlResponse = await _client.GetQueueUrlAsync(queue, cancellationToken); + return (queueUrlResponse.HttpStatusCode == HttpStatusCode.OK, queueUrlResponse.QueueUrl); + } + catch (QueueDoesNotExistException) + { + return (false, queue); + } + } + + /// + public void Dispose() => _client.Dispose(); +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByUrl.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByUrl.cs new file mode 100644 index 0000000000..4208877c64 --- /dev/null +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateQueueByUrl.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; + +namespace Paramore.Brighter.MessagingGateway.AWSSQS; + +/// +/// The class is responsible for validating an AWS SQS queue by its url. +/// +public class ValidateQueueByUrl : IValidateQueue, IDisposable +{ + private readonly AmazonSQSClient _client; + + /// + /// Initialize new instance of . + /// + /// The client. + public ValidateQueueByUrl(AmazonSQSClient client) + { + _client = client; + } + + /// + public async Task<(bool, string?)> ValidateAsync(string queue, CancellationToken cancellationToken = default) + { + try + { + _ = await _client.GetQueueAttributesAsync(queue, [QueueAttributeName.QueueArn], cancellationToken); + return (true, queue); + } + catch (QueueDoesNotExistException) + { + return (false, queue); + } + } + + /// + public void Dispose() => _client.Dispose(); +} diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs index 6c5e86a35a..3d4872103d 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByArnConvention.cs @@ -39,6 +39,7 @@ public class ValidateTopicByArnConvention : ValidateTopicByArn, IValidateTopic { private readonly RegionEndpoint _region; private readonly AmazonSecurityTokenServiceClient _stsClient; + private readonly SnsSqsType _type; /// /// Initializes a new instance of the class. @@ -46,10 +47,11 @@ public class ValidateTopicByArnConvention : ValidateTopicByArn, IValidateTopic /// The AWS credentials. /// The AWS region. /// An optional action to configure the client. - public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) + public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null, SnsSqsType type = SnsSqsType.Standard) : base(credentials, region, clientConfigAction) { _region = region; + _type = type; var clientFactory = new AWSClientFactory(credentials, region, clientConfigAction); _stsClient = clientFactory.CreateStsClient(); @@ -64,7 +66,7 @@ public ValidateTopicByArnConvention(AWSCredentials credentials, RegionEndpoint r public override async Task<(bool, string? TopicArn)> ValidateAsync(string topic, CancellationToken cancellationToken = default) { var topicArn = await GetArnFromTopic(topic); - return await base.ValidateAsync(topicArn); + return await base.ValidateAsync(topicArn, cancellationToken); } /// @@ -81,6 +83,8 @@ private async Task GetArnFromTopic(string topicName) if (callerIdentityResponse.HttpStatusCode != HttpStatusCode.OK) throw new InvalidOperationException("Could not find identity of AWS account"); + topicName = topicName.ToValidSNSTopicName(_type == SnsSqsType.Fifo); + return new Arn { Partition = _region.PartitionName, diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs index f7f1b4b892..766d60f718 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/ValidateTopicByName.cs @@ -36,6 +36,7 @@ namespace Paramore.Brighter.MessagingGateway.AWSSQS internal class ValidateTopicByName : IValidateTopic { private readonly AmazonSimpleNotificationServiceClient _snsClient; + private readonly SnsSqsType _type; /// /// Initializes a new instance of the class. @@ -43,19 +44,23 @@ internal class ValidateTopicByName : IValidateTopic /// The AWS credentials. /// The AWS region. /// An optional action to configure the client. - public ValidateTopicByName(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null) + /// The SNS Type. + public ValidateTopicByName(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction = null, SnsSqsType type = SnsSqsType.Standard) { var clientFactory = new AWSClientFactory(credentials, region, clientConfigAction); _snsClient = clientFactory.CreateSnsClient(); + _type = type; } /// /// Initializes a new instance of the class. /// /// The SNS client. - public ValidateTopicByName(AmazonSimpleNotificationServiceClient snsClient) + /// The SNS Type. + public ValidateTopicByName(AmazonSimpleNotificationServiceClient snsClient, SnsSqsType type = SnsSqsType.Standard) { _snsClient = snsClient; + _type = type; } /// @@ -71,6 +76,7 @@ public ValidateTopicByName(AmazonSimpleNotificationServiceClient snsClient) /// public async Task<(bool, string? TopicArn)> ValidateAsync(string topicName, CancellationToken cancellationToken = default) { + topicName = topicName.ToValidSNSTopicName(_type == SnsSqsType.Fifo); var topic = await _snsClient.FindTopicAsync(topicName); return (topic != null, topic?.TopicArn); } diff --git a/src/Paramore.Brighter.Tranformers.AWS/AWSS3Connection.cs b/src/Paramore.Brighter.Tranformers.AWS/AWSS3Connection.cs index e0c256a944..2d43c15bf4 100644 --- a/src/Paramore.Brighter.Tranformers.AWS/AWSS3Connection.cs +++ b/src/Paramore.Brighter.Tranformers.AWS/AWSS3Connection.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,32 +20,35 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - + #endregion +#nullable enable +using System; using Amazon; using Amazon.Runtime; -namespace Paramore.Brighter.Tranformers.AWS +namespace Paramore.Brighter.Tranformers.AWS; + +/// +/// Used to create an AWS Client +/// +public class AWSS3Connection { /// - /// Used to create an AWS Client + /// Constructs a credentials instance /// - public class AWSS3Connection + /// A credentials object for an AWS service + /// The AWS region to connect to + /// The AWS client configuration. + public AWSS3Connection(AWSCredentials credentials, RegionEndpoint region, Action? clientConfig = null) { - /// - /// Constructs a credentials instance - /// - /// A credentials object for an AWS service - /// The AWS region to connect to - public AWSS3Connection(AWSCredentials credentials, RegionEndpoint region) - { - Credentials = credentials; - Region = region; - } - - public AWSCredentials Credentials { get; } - public RegionEndpoint Region { get; } - + Credentials = credentials; + Region = region; + ClientConfig = clientConfig; } + + public AWSCredentials Credentials { get; } + public RegionEndpoint Region { get; } + public Action? ClientConfig { get; } } diff --git a/src/Paramore.Brighter.Tranformers.AWS/S3LuggageOptions.cs b/src/Paramore.Brighter.Tranformers.AWS/S3LuggageOptions.cs index 904133dd13..f7e37ddfdb 100644 --- a/src/Paramore.Brighter.Tranformers.AWS/S3LuggageOptions.cs +++ b/src/Paramore.Brighter.Tranformers.AWS/S3LuggageOptions.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,7 +20,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - + #endregion using System.Collections.Generic; @@ -53,7 +54,7 @@ public S3LuggageOptions(IHttpClientFactory httpClientFactory) TimeToAbortFailedUploads = 1; TimeToDeleteGoodUploads = 7; } - + /// /// How should we control access to the bucket used by the Luggage Store /// @@ -66,8 +67,13 @@ public AWSS3Connection Connection { set { - Client = new AmazonS3Client(value.Credentials, value.Region); - StsClient = new AmazonSecurityTokenServiceClient(value.Credentials, value.Region); + var s3Config = new AmazonS3Config { RegionEndpoint = value.Region }; + value.ClientConfig?.Invoke(s3Config); + Client = new AmazonS3Client(value.Credentials, s3Config); + + var stsConfig = new AmazonSecurityTokenServiceConfig { RegionEndpoint = value.Region }; + value.ClientConfig?.Invoke(stsConfig); + StsClient = new AmazonSecurityTokenServiceClient(value.Credentials, stsConfig); } } @@ -75,24 +81,24 @@ public AWSS3Connection Connection /// The name of the bucket, which will need to be unique within the AWS region /// public string BucketName { get; set; } - + /// /// The AWS region to create the bucket in /// public S3Region BucketRegion { get; set; } - + /// /// Get the AWS client created from the credentials passed into /// public IAmazonS3 Client { get; private set; } - + /// /// An HTTP client factory. We use this to grab an HTTP client, so that we can check if the bucket exists. /// Not required if you choose a of Assume Exists. /// We obtain this from the ServiceProvider when constructing the luggage store. so you do not need to set it /// public IHttpClientFactory HttpClientFactory { get; private set; } - + /// /// What Store Creation Option do you want: /// 1: Create @@ -100,22 +106,22 @@ public AWSS3Connection Connection /// 3: Assume it exists /// public S3LuggageStoreCreation StoreCreation { get; set; } - + /// /// The Security Token Service created from the credentials. Used to obtain the account id of the user with those credentials /// public IAmazonSecurityTokenService StsClient { get; private set; } - + /// /// Tags for the bucket. Defaults to a Creator tag of "Brighter Luggage Store" /// public List Tags { get; set; } - + /// /// How long to keep aborted uploads before deleting them in days /// public int TimeToAbortFailedUploads { get; set; } - + /// /// How long to keep good uploads in days, before deleting them /// diff --git a/src/Paramore.Brighter/CodeAnalysis/MemberNotNullWhenAttribute.cs b/src/Paramore.Brighter/CodeAnalysis/MemberNotNullWhenAttribute.cs new file mode 100644 index 0000000000..b1d85549e6 --- /dev/null +++ b/src/Paramore.Brighter/CodeAnalysis/MemberNotNullWhenAttribute.cs @@ -0,0 +1,40 @@ +#if NETSTANDARD +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] +public sealed class MemberNotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition and a field or property member. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The field or property member that is promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + ReturnValue = returnValue; + Members = new[] { member }; + } + + /// Initializes the attribute with the specified return value condition and list of field and property members. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + /// + /// The list of field and property members that are promised to be not-null. + /// + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + ReturnValue = returnValue; + Members = members; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + + /// Gets field or property member names. + public string[] Members { get; } +} + +#endif diff --git a/src/Paramore.Brighter/CodeAnalysis/NotNullWhenAttribute.cs b/src/Paramore.Brighter/CodeAnalysis/NotNullWhenAttribute.cs new file mode 100644 index 0000000000..0bdcb6353a --- /dev/null +++ b/src/Paramore.Brighter/CodeAnalysis/NotNullWhenAttribute.cs @@ -0,0 +1,17 @@ +#if NETSTANDARD +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +public sealed class NotNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +#endif diff --git a/src/Paramore.Brighter/RoutingKey.cs b/src/Paramore.Brighter/RoutingKey.cs index 89832d97b8..ef7f6546ba 100644 --- a/src/Paramore.Brighter/RoutingKey.cs +++ b/src/Paramore.Brighter/RoutingKey.cs @@ -22,6 +22,8 @@ THE SOFTWARE. */ #endregion +using System.Diagnostics.CodeAnalysis; + namespace Paramore.Brighter { /// @@ -49,7 +51,7 @@ public RoutingKey(string name) /// /// The routing key to test /// - public static bool IsNullOrEmpty(RoutingKey? routingKey) + public static bool IsNullOrEmpty([NotNullWhen(false)]RoutingKey? routingKey) { return routingKey is null || string.IsNullOrEmpty(routingKey.Value); } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs new file mode 100644 index 0000000000..ac321abff4 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/AWSClientFactory.cs @@ -0,0 +1,104 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2022 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.SecurityToken; +using Amazon.SimpleNotificationService; +using Amazon.SQS; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Paramore.Brighter.AWS.Tests.Helpers; + +internal class AWSClientFactory +{ + private readonly AWSCredentials _credentials; + private readonly RegionEndpoint _region; + private readonly Action? _clientConfigAction; + + public AWSClientFactory(AWSMessagingGatewayConnection connection) + { + _credentials = connection.Credentials; + _region = connection.Region; + _clientConfigAction = connection.ClientConfigAction; + } + + public AWSClientFactory(AWSCredentials credentials, RegionEndpoint region, Action? clientConfigAction) + { + _credentials = credentials; + _region = region; + _clientConfigAction = clientConfigAction; + } + + public AmazonSimpleNotificationServiceClient CreateSnsClient() + { + var config = new AmazonSimpleNotificationServiceConfig { RegionEndpoint = _region }; + + if (_clientConfigAction != null) + { + _clientConfigAction(config); + } + + return new AmazonSimpleNotificationServiceClient(_credentials, config); + } + + public AmazonSQSClient CreateSqsClient() + { + var config = new AmazonSQSConfig { RegionEndpoint = _region }; + + if (_clientConfigAction != null) + { + _clientConfigAction(config); + } + + return new AmazonSQSClient(_credentials, config); + } + + public AmazonSecurityTokenServiceClient CreateStsClient() + { + var config = new AmazonSecurityTokenServiceConfig { RegionEndpoint = _region }; + + if (_clientConfigAction != null) + { + _clientConfigAction(config); + } + + return new AmazonSecurityTokenServiceClient(_credentials, config); + } + + public AmazonS3Client CreateS3Client() + { + var config = new AmazonS3Config { RegionEndpoint = _region }; + + if (_clientConfigAction != null) + { + _clientConfigAction(config); + } + + return new AmazonS3Client(_credentials, config); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs new file mode 100644 index 0000000000..76cc08cf87 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/GatewayFactory.cs @@ -0,0 +1,40 @@ +using System; +using System.Configuration; +using Amazon; +using Amazon.Runtime; +using Paramore.Brighter.MessagingGateway.AWSSQS; + +namespace Paramore.Brighter.AWS.Tests.Helpers; + +public class GatewayFactory +{ + public static AWSMessagingGatewayConnection CreateFactory() + { + var (credentials, region) = CredentialsChain.GetAwsCredentials(); + return CreateFactory(credentials, region, config => { }); + } + + public static AWSMessagingGatewayConnection CreateFactory(Action clientConfig) + { + var (credentials, region) = CredentialsChain.GetAwsCredentials(); + return CreateFactory(credentials, region, clientConfig); + } + + public static AWSMessagingGatewayConnection CreateFactory( + AWSCredentials credentials, + RegionEndpoint region, + Action? config = null) + { + return new AWSMessagingGatewayConnection(credentials, region, + cfg => + { + config?.Invoke(cfg); + + var serviceURL = Environment.GetEnvironmentVariable("LOCALSTACK_SERVICE_URL"); + if (!string.IsNullOrWhiteSpace(serviceURL)) + { + cfg.ServiceURL = serviceURL; + } + }); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingDelegatingHandler.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingDelegatingHandler.cs index 01fca0d7ad..255f61d76a 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingDelegatingHandler.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingDelegatingHandler.cs @@ -1,18 +1,21 @@ -using System.Net.Http; +using System.Collections.Concurrent; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -namespace Paramore.Brighter.AWS.Tests.Helpers +namespace Paramore.Brighter.AWS.Tests.Helpers; + +internal class InterceptingDelegatingHandler(string tag) : DelegatingHandler { - internal class InterceptingDelegatingHandler : DelegatingHandler - { - public int RequestCount { get; private set; } + public static ConcurrentDictionary RequestCount { get; } = new(); - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (!RequestCount.TryAdd(tag, 1)) { - RequestCount++; - - return await base.SendAsync(request, cancellationToken); + RequestCount[tag] += 1; } + + return await base.SendAsync(request, cancellationToken); } } diff --git a/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingHttpClientFactory.cs b/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingHttpClientFactory.cs index c30cab2e40..83ba274dfe 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingHttpClientFactory.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Helpers/InterceptingHttpClientFactory.cs @@ -1,20 +1,13 @@ using System.Net.Http; using Amazon.Runtime; -namespace Paramore.Brighter.AWS.Tests.Helpers +namespace Paramore.Brighter.AWS.Tests.Helpers; + +internal class InterceptingHttpClientFactory(InterceptingDelegatingHandler handler) : HttpClientFactory { - internal class InterceptingHttpClientFactory : HttpClientFactory + public override HttpClient CreateHttpClient(IClientConfig clientConfig) { - private readonly InterceptingDelegatingHandler _handler; - - public InterceptingHttpClientFactory(InterceptingDelegatingHandler handler) - { - _handler = handler; - } - - public override HttpClient CreateHttpClient(IClientConfig clientConfig) - { - return new HttpClient(_handler); - } + handler.InnerHandler ??= new HttpClientHandler(); + return new HttpClient(handler); } } diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..e87f86e43f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + deduplicationScope: DeduplicationScope.MessageGroup, + fifoThroughputLimit: 1 + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages_async() + { + var routingKey = new RoutingKey(_topicName); + + var messageGroupIdOne = $"MessageGroup{Guid.NewGuid():N}"; + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content two") + ); + + var messageGroupIdTwo = $"MessageGroup{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content four") + ); + + var messageFive = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + await _messageProducer.SendAsync(messageOne); + await _messageProducer.SendAsync(messageTwo); + await _messageProducer.SendAsync(messageThree); + await _messageProducer.SendAsync(messageFour); + await _messageProducer.SendAsync(messageFive); + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + await _consumer.AcknowledgeAsync(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + messagesReceivedCount.Should().Be(4); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.DisposeAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs new file mode 100644 index 0000000000..e1df37baf2 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Assume, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true)); + } + + [Fact] + public async Task When_infastructure_exists_can_assume() + { + //arrange + await _messageProducer.SendAsync(_message); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs new file mode 100644 index 0000000000..324bfc228b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(topicName), + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs new file mode 100644 index 0000000000..859a582351 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByArnTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByArnTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + var topicArn = FindTopicArn(awsConnection, routingKey.ToValidSNSTopicName(true)).Result; + var routingKeyArn = new RoutingKey(topicArn); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKeyArn, + findTopicBy: TopicFindBy.Arn, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + Topic = routingKey, + TopicArn = topicArn, + FindTopicBy = TopicFindBy.Arn, + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + private static async Task FindTopicArn(AWSMessagingGatewayConnection connection, string topicName) + { + using var snsClient = new AWSClientFactory(connection).CreateSnsClient(); + var topicResponse = await snsClient.FindTopicAsync(topicName); + return topicResponse.TopicArn; + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..0055719ad2 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,127 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _topicName; + private readonly string _messageGroupId; + private readonly string _deduplicationId; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + _messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + _deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + rawMessageDelivery: false, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType, partitionKey: _messageGroupId) + { + Bag = { [HeaderNames.DeduplicationId] = _deduplicationId } + }, + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + Topic = new RoutingKey(_topicName), + MakeChannels = OnMissingChannel.Create, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer_async() + { + // arrange + _message.Header.Subject = "test subject"; + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_topicName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + + message.Header.PartitionKey.Should().Be(_messageGroupId); + message.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + message.Header.Bag[HeaderNames.DeduplicationId].Should().Be(_deduplicationId); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_assume_throws_async.cs new file mode 100644 index 0000000000..1e4cc87099 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_assume_throws_async.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageConsumerAsync _consumer; + + public AWSAssumeQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Assume, + messagePumpType: MessagePumpType.Proactor, + sqsType: SnsSqsType.Fifo + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_queues_missing_assume_throws_async() + { + //we will try to get the queue url, and fail because it does not exist + await Assert.ThrowsAsync(async () => + await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_verify_throws_async.cs new file mode 100644 index 0000000000..63532ac3a9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_queues_missing_verify_throws_async.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTestsAsync : IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + // We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + producer.ConfirmTopicExistsAsync(topicName).Wait(); + } + + [Fact] + public async Task When_queues_missing_verify_throws_async() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + await Assert.ThrowsAsync(async () => + await _channelFactory.CreateAsyncChannelAsync(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs new file mode 100644 index 0000000000..243ce52948 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsRawMessageDeliveryTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelAsync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + const int bufferSize = 10; + + // Set rawMessageDelivery to false + _channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: _routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + rawMessageDelivery: false, + sqsType: SnsSqsType.Fifo)); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_raw_message_delivery_disabled_async() + { + // Arrange + var messageGroupId = $"MessageGroupId{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain", + partitionKey: messageGroupId) { Bag = { [HeaderNames.DeduplicationId] = deduplicationId } }; + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSend = new Message(messageHeader, new MessageBody("test content one")); + + // Act + await _messageProducer.SendAsync(messageToSend); + + var messageReceived = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + await _channel.AcknowledgeAsync(messageReceived); + + // Assert + messageReceived.Id.Should().Be(messageToSend.Id); + messageReceived.Header.Topic.Should().Be(messageToSend.Header.Topic.ToValidSNSTopicName(true)); + messageReceived.Header.MessageType.Should().Be(messageToSend.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSend.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSend.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSend.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSend.Body.Value); + messageReceived.Header.PartitionKey.Should().Be(messageGroupId); + messageReceived.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + messageReceived.Header.Bag[HeaderNames.DeduplicationId].Should().Be(deduplicationId); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs new file mode 100644 index 0000000000..d70b0d4a22 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_rejecting_a_message_through_gateway_with_requeue_async() + { + await _messageProducer.SendAsync(_message); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.RejectAsync(message); + + // Let the timeout change + await Task.Delay(TimeSpan.FromMilliseconds(3000)); + + // should requeue_the_message + message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_a_message_async.cs new file mode 100644 index 0000000000..7b1d37577b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_a_message_async.cs @@ -0,0 +1,85 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await _sender.SendAsync(_message); + _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(_receivedMessage); + + _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.AcknowledgeAsync(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs new file mode 100644 index 0000000000..2c2af76335 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _sender; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2), + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_redrives_to_the_queue_async() + { + await _sender.SendAsync(_message); + var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName + ".fifo"); + dlqCount.Should().Be(1); + } + + private async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs new file mode 100644 index 0000000000..19b6563b65 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTestsAsync() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2) + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer( + _awsConnection, + new SnsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Proactor(commandProcessor, messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, + TimeOut = TimeSpan.FromMilliseconds(5000), + RequeueCount = 3 + }; + } + + public async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, + MessageAttributeNames = new List { "All" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public async Task When_throwing_defer_action_respect_redrive_async() + { + await _sender.SendAsync(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + await Task.Delay(5000); + + var dlqCount = await GetDLQCountAsync(_dlqChannelName + ".fifo"); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_topic_missing_verify_throws_async.cs new file mode 100644 index 0000000000..9d1d5e4339 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Proactor/When_topic_missing_verify_throws_async.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTestsAsync +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTestsAsync() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + _awsConnection = GatewayFactory.CreateFactory(); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public async Task When_topic_missing_verify_throws_async() + { + // arrange + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + + // act & assert + await Assert.ThrowsAsync(async () => + await producer.SendAsync(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, + type: "plain/text", partitionKey: messageGroupId), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs new file mode 100644 index 0000000000..ce5dd28b5f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + _channelFactory = new ChannelFactory(awsConnection); + + var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + contentBasedDeduplication: true, + deduplicationScope: DeduplicationScope.MessageGroup, + fifoThroughputLimit: 1 + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages() + { + var routingKey = new RoutingKey(_topicName); + + var messageGroupIdOne = $"MessageGroup{Guid.NewGuid():N}"; + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content two") + ); + + + var messageGroupIdTwo = $"MessageGroup{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content four") + ); + + var messageFive = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + _messageProducer.Send(messageOne); + _messageProducer.Send(messageTwo); + _messageProducer.Send(messageThree); + _messageProducer.Send(messageFour); + _messageProducer.Send(messageFive); + + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + _consumer.Acknowledge(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + + messagesReceivedCount.Should().Be(4); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageProducerAsync)_messageProducer).DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_assume.cs new file mode 100644 index 0000000000..637948d139 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_assume.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Assume, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true)); + } + + [Fact] + public void When_infastructure_exists_can_assume() + { + //arrange + _messageProducer.Send(_message); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify.cs new file mode 100644 index 0000000000..1e97b00a8c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(topicName), + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_arn.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_arn.cs new file mode 100644 index 0000000000..defbefe047 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_arn.cs @@ -0,0 +1,126 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByArnTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByArnTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _myCommand = new MyCommand { Value = "Test" }; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + var topicArn = FindTopicArn(awsConnection, routingKey.ToValidSNSTopicName(true)); + var routingKeyArn = new RoutingKey(topicArn); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKeyArn, + findTopicBy: TopicFindBy.Arn, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + Topic = routingKey, + TopicArn = topicArn, + FindTopicBy = TopicFindBy.Arn, + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + + private static string FindTopicArn(AWSMessagingGatewayConnection connection, string topicName) + { + using var snsClient = new AWSClientFactory(connection).CreateSnsClient(); + var topicResponse = snsClient.FindTopicAsync(topicName).GetAwaiter().GetResult(); + return topicResponse.TopicArn; + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_convention.cs new file mode 100644 index 0000000000..dd985931d6 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infastructure_exists_can_verify_by_convention.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByConventionTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByConventionTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made - will make the SNS Arn to prevent ListTopics call + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infrastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infrastructure_exists_can_verify_by_convention.cs new file mode 100644 index 0000000000..b0904b6d6d --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_infrastructure_exists_can_verify_by_convention.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByConventionTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByConventionTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs new file mode 100644 index 0000000000..73445afec9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs @@ -0,0 +1,127 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _topicName; + private readonly string _messageGroupId; + private readonly string _deduplicationId; + + public SqsMessageProducerSendTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + _deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + rawMessageDelivery: false, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType, partitionKey: _messageGroupId) + { + Bag = { [HeaderNames.DeduplicationId] = _deduplicationId } + }, + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + Topic = new RoutingKey(_topicName), + MakeChannels = OnMissingChannel.Create, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer() + { + //arrange + _message.Header.Subject = "test subject"; + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + //should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_topicName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + //allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + //{"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + + message.Header.PartitionKey.Should().Be(_messageGroupId); + message.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + message.Header.Bag[HeaderNames.DeduplicationId].Should().Be(_deduplicationId); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_assume_throws.cs new file mode 100644 index 0000000000..80a5b85dcf --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_assume_throws.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly SqsMessageConsumer _consumer; + + public AWSAssumeQueuesTests() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_queues_missing_assume_throws() + { + //we will try to get the queue url, and fail because it does not exist + Assert.Throws(() => _consumer.Receive(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_verify_throws.cs new file mode 100644 index 0000000000..c4ccaa6b86 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_queues_missing_verify_throws.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTests() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + producer.ConfirmTopicExistsAsync(topicName).Wait(); + } + + [Fact] + public void When_queues_missing_verify_throws() + { + //We have no queues so we should throw + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + Assert.Throws(() => _channelFactory.CreateSyncChannel(_subscription)); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_raw_message_delivery_disabled.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_raw_message_delivery_disabled.cs new file mode 100644 index 0000000000..c83e0c6a76 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_raw_message_delivery_disabled.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsRawMessageDeliveryTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelSync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var bufferSize = 10; + + //Set rawMessageDelivery to false + _channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: _routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + messagePumpType: MessagePumpType.Reactor, + rawMessageDelivery: false, + sqsType: SnsSqsType.Fifo)); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public void When_raw_message_delivery_disabled() + { + //arrange + var messageGroupId = $"MessageGroupId{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain", + partitionKey: messageGroupId) { Bag = { [HeaderNames.DeduplicationId] = deduplicationId } }; + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSent = new Message(messageHeader, new MessageBody("test content one")); + + //act + _messageProducer.Send(messageToSent); + + var messageReceived = _channel.Receive(TimeSpan.FromMilliseconds(10000)); + + _channel.Acknowledge(messageReceived); + + //assert + messageReceived.Id.Should().Be(messageToSent.Id); + messageReceived.Header.Topic.Should().Be(messageToSent.Header.Topic.ToValidSNSTopicName(true)); + messageReceived.Header.MessageType.Should().Be(messageToSent.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSent.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSent.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSent.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSent.Body.Value); + + messageReceived.Header.PartitionKey.Should().Be(messageGroupId); + messageReceived.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + messageReceived.Header.Bag[HeaderNames.DeduplicationId].Should().Be(deduplicationId); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs new file mode 100644 index 0000000000..e3d4b80637 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs @@ -0,0 +1,93 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTests : IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Reactor, + routingKey: routingKey, + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public void When_rejecting_a_message_through_gateway_with_requeue() + { + _messageProducer.Send(_message); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + _channel.Reject(message); + + //Let the timeout change + Task.Delay(TimeSpan.FromMilliseconds(3000)); + + //should requeue_the_message + message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_a_message.cs new file mode 100644 index 0000000000..6e74cb514b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_a_message.cs @@ -0,0 +1,85 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerSync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_a_message() + { + _sender.Send(_message); + _receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(_receivedMessage); + + _requeuedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs new file mode 100644 index 0000000000..6eebf5b660 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs @@ -0,0 +1,119 @@ +using System; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _sender; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2), + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_redrives_to_the_queue() + { + _sender.Send(_message); + var receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + //should force us into the dlq + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName + ".fifo").Should().Be(1); + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = ["All", "ApproximateReceiveCount"] + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs new file mode 100644 index 0000000000..bc52e56776 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs @@ -0,0 +1,167 @@ +using System; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + //how are we consuming + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + //don't block the redrive policy from owning retry management + requeueCount: -1, + //delay before requeuing + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Reactor, + //we want our SNS subscription to manage requeue limits using the DLQ for 'too many requeues' + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2), + sqsType: SnsSqsType.Fifo); + + //what do we send + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + //how do we send to the queue + _sender = new SnsMessageProducer( + _awsConnection, + new SnsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + } + ); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(_subscription); + + //how do we handle a command + IHandleRequests handler = new MyDeferredCommandHandler(); + + //hook up routing for the command processor + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.Register(); + + //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactory(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test + _messagePump = new Reactor(commandProcessor, messageMapperRegistry, + null, new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = ["ApproximateReceiveCount"], + MessageAttributeNames = ["All"] + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + + [Fact] + public async Task When_throwing_defer_action_respect_redrive() + { + //put something on an SNS topic, which will be delivered to our SQS queue + _sender.Send(_message); + + //start a message pump, let it process messages + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + //send a quit message to the pump to terminate it + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + //wait for the pump to stop once it gets a quit message + await Task.WhenAll(task); + + await Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName + ".fifo").Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_topic_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_topic_missing_verify_throws.cs new file mode 100644 index 0000000000..98a944df4a --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Fifo/Reactor/When_topic_missing_verify_throws.cs @@ -0,0 +1,43 @@ +using System; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTests +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTests() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + _awsConnection = GatewayFactory.CreateFactory(); + + //Because we don't use channel factory to create the infrastructure -it won't exist + } + + [Fact] + public void When_topic_missing_verify_throws() + { + //arrange + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Validate, + SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + + //act && assert + Assert.Throws(() => producer.Send(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, + type: "plain/text", partitionKey: messageGroupId), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..93c8afc1f3 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages_async() + { + var routingKey = new RoutingKey(_topicName); + + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content two") + ); + + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + await _messageProducer.SendAsync(messageOne); + await _messageProducer.SendAsync(messageTwo); + await _messageProducer.SendAsync(messageThree); + await _messageProducer.SendAsync(messageFour); + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + await _consumer.AcknowledgeAsync(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + messagesReceivedCount.Should().Be(4); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.DisposeAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_customising_aws_client_config_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_customising_aws_client_config_async.cs new file mode 100644 index 0000000000..8e015cdb84 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_customising_aws_client_config_async.cs @@ -0,0 +1,95 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class CustomisingAwsClientConfigTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + + public CustomisingAwsClientConfigTestsAsync() + { + MyCommand myCommand = new() { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + string correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Proactor, + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var subscribeAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = + new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("async_sub")); + }); + + _channelFactory = new ChannelFactory(subscribeAwsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + var publishAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = + new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("async_pub")); + }); + + _messageProducer = new SnsMessageProducer(publishAwsConnection, + new SnsPublication { Topic = new RoutingKey(topicName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_customising_aws_client_config() + { + //arrange + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + await _channel.AcknowledgeAsync(message); + + //publish_and_subscribe_should_use_custom_http_client_factory + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("async_sub"); + InterceptingDelegatingHandler.RequestCount["async_sub"].Should().BeGreaterThan(0); + + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("async_pub"); + InterceptingDelegatingHandler.RequestCount["async_pub"].Should().BeGreaterThan(0); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_assume_async.cs new file mode 100644 index 0000000000..85ae4a3169 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_assume_async.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTestsAsync() + { + _myCommand = new MyCommand{Value = "Test"}; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume + ); + + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Assume}); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public async Task When_infastructure_exists_can_assume() + { + //arrange + await _messageProducer.SendAsync(_message); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_verify_by_arn.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_verify_by_arn.cs new file mode 100644 index 0000000000..a1f2e3e3b6 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infastructure_exists_can_verify_by_arn.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByArnTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByArnTests() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = GatewayFactory.CreateFactory(credentials, region); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + var topicArn = FindTopicArn(awsConnection, routingKey.Value); + var routingKeyArn = new RoutingKey(topicArn); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKeyArn, + findTopicBy: TopicFindBy.Arn, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + Topic = routingKey, + TopicArn = topicArn, + FindTopicBy = TopicFindBy.Arn, + MakeChannels = OnMissingChannel.Validate + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + + private static string FindTopicArn(AWSMessagingGatewayConnection connection, string topicName) + { + using var snsClient = new AWSClientFactory(connection).CreateSnsClient(); + var topicResponse = snsClient.FindTopicAsync(topicName).GetAwaiter().GetResult(); + return topicResponse.TopicArn; + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs similarity index 90% rename from tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs rename to tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs index fef735052b..37bbc50ffd 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_async.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs @@ -2,15 +2,13 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using FluentAssertions; using Paramore.Brighter.AWS.Tests.Helpers; using Paramore.Brighter.AWS.Tests.TestDoubles; using Paramore.Brighter.MessagingGateway.AWSSQS; using Xunit; -namespace Paramore.Brighter.AWS.Tests.MessagingGateway +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] @@ -18,7 +16,7 @@ public class AWSValidateInfrastructureTestsAsync : IDisposable, IAsyncDisposable { private readonly Message _message; private readonly IAmAMessageConsumerAsync _consumer; - private readonly SqsMessageProducer _messageProducer; + private readonly SnsMessageProducer _messageProducer; private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; @@ -46,8 +44,7 @@ public AWSValidateInfrastructureTestsAsync() new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) ); - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + var awsConnection = GatewayFactory.CreateFactory(); _channelFactory = new ChannelFactory(awsConnection); var channel = _channelFactory.CreateAsyncChannel(subscription); @@ -61,7 +58,7 @@ public AWSValidateInfrastructureTestsAsync() makeChannels: OnMissingChannel.Validate ); - _messageProducer = new SqsMessageProducer( + _messageProducer = new SnsMessageProducer( awsConnection, new SnsPublication { diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs new file mode 100644 index 0000000000..eb74572536 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_infrastructure_exists_can_verify_by_arn_async.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByArnTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByArnTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var awsConnection = GatewayFactory.CreateFactory(credentials, region); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + var topicArn = FindTopicArn(awsConnection, routingKey.Value).Result; + var routingKeyArn = new RoutingKey(topicArn); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKeyArn, + findTopicBy: TopicFindBy.Arn, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + Topic = routingKey, + TopicArn = topicArn, + FindTopicBy = TopicFindBy.Arn, + MakeChannels = OnMissingChannel.Validate + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + private static async Task FindTopicArn(AWSMessagingGatewayConnection connection, string topicName) + { + using var snsClient = new AWSClientFactory(connection).CreateSnsClient(); + var topicResponse = await snsClient.FindTopicAsync(topicName); + return topicResponse.TopicArn; + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..3aabf7a96b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _topicName; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + rawMessageDelivery: false + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication { Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer_async() + { + // arrange + _message.Header.Subject = "test subject"; + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_topicName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_assume_throws_async.cs new file mode 100644 index 0000000000..00ebc34930 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_assume_throws_async.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageConsumerAsync _consumer; + + public AWSAssumeQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Assume, + messagePumpType: MessagePumpType.Proactor + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_queues_missing_assume_throws_async() + { + //we will try to get the queue url, and fail because it does not exist + await Assert.ThrowsAsync(async () => await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_verify_throws_async.cs new file mode 100644 index 0000000000..4f3ce55ef5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_queues_missing_verify_throws_async.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTestsAsync : IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + // We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + producer.ConfirmTopicExistsAsync(topicName).Wait(); + } + + [Fact] + public async Task When_queues_missing_verify_throws_async() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + await Assert.ThrowsAsync(async () => await _channelFactory.CreateAsyncChannelAsync(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_raw_message_delivery_disabled_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_raw_message_delivery_disabled_async.cs new file mode 100644 index 0000000000..d4352dd889 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_raw_message_delivery_disabled_async.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsRawMessageDeliveryTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelAsync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var bufferSize = 10; + + // Set rawMessageDelivery to false + _channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: _routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + rawMessageDelivery: false)); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + } + + [Fact] + public async Task When_raw_message_delivery_disabled_async() + { + // Arrange + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain"); + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSend = new Message(messageHeader, new MessageBody("test content one")); + + // Act + await _messageProducer.SendAsync(messageToSend); + + var messageReceived = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + await _channel.AcknowledgeAsync(messageReceived); + + // Assert + messageReceived.Id.Should().Be(messageToSend.Id); + messageReceived.Header.Topic.Should().Be(messageToSend.Header.Topic); + messageReceived.Header.MessageType.Should().Be(messageToSend.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSend.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSend.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSend.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSend.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs new file mode 100644 index 0000000000..cf1691c408 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_rejecting_a_message_through_gateway_with_requeue_async() + { + await _messageProducer.SendAsync(_message); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.RejectAsync(message); + + // Let the timeout change + await Task.Delay(TimeSpan.FromMilliseconds(3000)); + + // should requeue_the_message + message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_a_message_async.cs new file mode 100644 index 0000000000..81b7ba11e9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_a_message_async.cs @@ -0,0 +1,82 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Runtime.CredentialManagement; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + new CredentialProfileStoreChain(); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await _sender.SendAsync(_message); + _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(_receivedMessage); + + _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.AcknowledgeAsync(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs new file mode 100644 index 0000000000..923ee62abe --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _sender; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(_awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_redrives_to_the_queue_async() + { + await _sender.SendAsync(_message); + var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + private async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs new file mode 100644 index 0000000000..e56e024ed9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelAsync _channel; + private readonly SnsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTestsAsync() + { + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2) + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer( + _awsConnection, + new SnsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Proactor(commandProcessor , messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + public async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, + MessageAttributeNames = new List { "All" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public async Task When_throwing_defer_action_respect_redrive_async() + { + await _sender.SendAsync(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_topic_missing_verify_throws_async.cs new file mode 100644 index 0000000000..9e05510a16 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Proactor/When_topic_missing_verify_throws_async.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTestsAsync +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTestsAsync() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + _awsConnection = GatewayFactory.CreateFactory(); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public async Task When_topic_missing_verify_throws_async() + { + // arrange + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Validate + }); + + // act & assert + await Assert.ThrowsAsync(async () => + await producer.SendAsync(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs new file mode 100644 index 0000000000..0abbe10adf --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _topicName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_topicName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName:new ChannelName(channelName), + routingKey:routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages() + { + var routingKey = new RoutingKey(_topicName); + + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var messageTwo= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content two") + ); + + var messageThree= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content three") + ); + + var messageFour= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + _messageProducer.Send(messageOne); + _messageProducer.Send(messageTwo); + _messageProducer.Send(messageThree); + _messageProducer.Send(messageFour); + + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + _consumer.Acknowledge(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + + messagesReceivedCount.Should().Be(4); + + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageProducerAsync) _messageProducer).DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_customising_aws_client_config.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_customising_aws_client_config.cs new file mode 100644 index 0000000000..142e62fc57 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_customising_aws_client_config.cs @@ -0,0 +1,93 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class CustomisingAwsClientConfigTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + + public CustomisingAwsClientConfigTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + MyCommand myCommand = new() { Value = "Test" }; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Reactor, + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var subscribeAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sync_sub")); + }); + + _channelFactory = new ChannelFactory(subscribeAwsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + var publishAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sync_pub")); + }); + + _messageProducer = new SnsMessageProducer(publishAwsConnection, + new SnsPublication { Topic = new RoutingKey(topicName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_customising_aws_client_config() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + //publish_and_subscribe_should_use_custom_http_client_factory + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("sync_sub"); + InterceptingDelegatingHandler.RequestCount["sync_sub"].Should().BeGreaterThan(0); + + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("sync_pub"); + InterceptingDelegatingHandler.RequestCount["sync_pub"].Should().BeGreaterThan(0); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_assume.cs new file mode 100644 index 0000000000..12183aabd5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_assume.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume + ); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication { MakeChannels = OnMissingChannel.Assume }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_infastructure_exists_can_assume() + { + //arrange + _messageProducer.Send(_message); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify.cs new file mode 100644 index 0000000000..bd69a5ea0d --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(topicName) + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify_by_convention.cs new file mode 100644 index 0000000000..aa64e963e9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infastructure_exists_can_verify_by_convention.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByConventionTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByConventionTests() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made - will make the SNS Arn to prevent ListTopics call + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication { FindTopicBy = TopicFindBy.Convention, MakeChannels = OnMissingChannel.Validate } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infrastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infrastructure_exists_can_verify_by_convention.cs new file mode 100644 index 0000000000..83b4dbf54c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_infrastructure_exists_can_verify_by_convention.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByConventionTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByConventionTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(channelName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Convention, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SnsMessageProducer( + awsConnection, + new SnsPublication + { + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Validate + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs new file mode 100644 index 0000000000..eee916b9da --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs @@ -0,0 +1,110 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _topicName; + + public SqsMessageProducerSendTests() + { + _myCommand = new MyCommand{Value = "Test"}; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + rawMessageDelivery: false + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication{Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create}); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer() + { + //arrange + _message.Header.Subject = "test subject"; + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + //should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_topicName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + //allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + //{"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } + +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_assume_throws.cs new file mode 100644 index 0000000000..69bedf1660 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_assume_throws.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly SqsMessageConsumer _consumer; + + public AWSAssumeQueuesTests() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_queues_missing_assume_throws() + { + //we will try to get the queue url, and fail because it does not exist + Assert.Throws(() => _consumer.Receive(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } + +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_verify_throws.cs new file mode 100644 index 0000000000..478fc908d3 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_queues_missing_verify_throws.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTests() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + } + + [Fact] + public void When_queues_missing_verify_throws() + { + //We have no queues so we should throw + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + Assert.Throws(() => _channelFactory.CreateSyncChannel(_subscription)); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_raw_message_delivery_disabled.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_raw_message_delivery_disabled.cs new file mode 100644 index 0000000000..6973f615a9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_raw_message_delivery_disabled.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsRawMessageDeliveryTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelSync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); + + var bufferSize = 10; + + //Set rawMessageDelivery to false + _channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(channelName), + channelName:new ChannelName(channelName), + routingKey:_routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + messagePumpType: MessagePumpType.Reactor, + rawMessageDelivery: false)); + + _messageProducer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + } + + [Fact] + public void When_raw_message_delivery_disabled() + { + //arrange + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain"); + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSent = new Message(messageHeader, new MessageBody("test content one")); + + //act + _messageProducer.Send(messageToSent); + + var messageReceived = _channel.Receive(TimeSpan.FromMilliseconds(10000)); + + _channel.Acknowledge(messageReceived); + + //assert + messageReceived.Id.Should().Be(messageToSent.Id); + messageReceived.Header.Topic.Should().Be(messageToSent.Header.Topic); + messageReceived.Header.MessageType.Should().Be(messageToSent.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSent.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSent.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSent.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSent.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs new file mode 100644 index 0000000000..f20ce2b29f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs @@ -0,0 +1,87 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTests : IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTests() + { + _myCommand = new MyCommand{Value = "Test"}; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + messagePumpType: MessagePumpType.Reactor, + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SnsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); + } + + [Fact] + public void When_rejecting_a_message_through_gateway_with_requeue() + { + _messageProducer.Send(_message); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + _channel.Reject(message); + + //Let the timeout change + Task.Delay(TimeSpan.FromMilliseconds(3000)); + + //should requeue_the_message + message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_a_message.cs new file mode 100644 index 0000000000..ed053b3c5a --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_a_message.cs @@ -0,0 +1,83 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Runtime.CredentialManagement; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerSync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTests() + { + MyCommand myCommand = new MyCommand{Value = "Test"}; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + new CredentialProfileStoreChain(); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_a_message() + { + _sender.Send(_message); + _receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(_receivedMessage); + + _requeuedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(_requeuedMessage ); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs new file mode 100644 index 0000000000..3b13e3ac6b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _sender; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + SqsSubscription subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(_awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_redrives_to_the_queue() + { + _sender.Send(_message); + var receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + //should force us into the dlq + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName).Should().Be(1); + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs new file mode 100644 index 0000000000..786098e690 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelSync _channel; + private readonly SnsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTests() + { + string correlationId = Guid.NewGuid().ToString(); + string replyTo = "http:\\queueUrl"; + string contentType = "text\\plain"; + var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + //how are we consuming + _subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + //don't block the redrive policy from owning retry management + requeueCount: -1, + //delay before requeuing + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Reactor, + //we want our SNS subscription to manage requeue limits using the DLQ for 'too many requeues' + redrivePolicy: new RedrivePolicy + ( + deadLetterQueueName: new ChannelName(_dlqChannelName), + maxReceiveCount: 2 + )); + + //what do we send + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + //how do we send to the queue + _sender = new SnsMessageProducer( + _awsConnection, + new SnsPublication + { + Topic = routingKey, RequestType = typeof(MyDeferredCommand), MakeChannels = OnMissingChannel.Create + } + ); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(_subscription); + + //how do we handle a command + IHandleRequests handler = new MyDeferredCommandHandler(); + + //hook up routing for the command processor + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.Register(); + + //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactory(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test + _messagePump = new Reactor(commandProcessor, messageMapperRegistry, + null, new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = ["ApproximateReceiveCount"], + MessageAttributeNames = new List { "All" } + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + + [Fact] + public async Task When_throwing_defer_action_respect_redrive() + { + //put something on an SNS topic, which will be delivered to our SQS queue + _sender.Send(_message); + + //start a message pump, let it process messages + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + //send a quit message to the pump to terminate it + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + //wait for the pump to stop once it gets a quit message + await Task.WhenAll(task); + + await Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName).Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_topic_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_topic_missing_verify_throws.cs new file mode 100644 index 0000000000..2b1a698030 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sns/Standard/Reactor/When_topic_missing_verify_throws.cs @@ -0,0 +1,39 @@ +using System; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sns.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTests +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTests() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + _awsConnection = GatewayFactory.CreateFactory(); + + //Because we don't use channel factory to create the infrastructure -it won't exist + } + + [Fact] + public void When_topic_missing_verify_throws() + { + //arrange + var producer = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Validate, + }); + + //act && assert + Assert.Throws(() => producer.Send(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..70ae15e034 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _queueName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(_queueName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + deduplicationScope: DeduplicationScope.MessageGroup, + fifoThroughputLimit: 1, + channelType: ChannelType.PointToPoint + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages_async() + { + var routingKey = new RoutingKey(_queueName); + + var messageGroupIdOne = $"MessageGroup{Guid.NewGuid():N}"; + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content two") + ); + + var messageGroupIdTwo = $"MessageGroup{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content four") + ); + + var messageFive = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + await _messageProducer.SendAsync(messageOne); + await _messageProducer.SendAsync(messageTwo); + await _messageProducer.SendAsync(messageThree); + await _messageProducer.SendAsync(messageFour); + await _messageProducer.SendAsync(messageFive); + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + await _consumer.AcknowledgeAsync(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + messagesReceivedCount.Should().Be(4); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.DisposeAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs new file mode 100644 index 0000000000..ebb1d2e0a5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infastructure_exists_can_assume_async.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(queueName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Assume, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true)); + } + + [Fact] + public async Task When_infastructure_exists_can_assume() + { + //arrange + await _messageProducer.SendAsync(_message); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs new file mode 100644 index 0000000000..1f62312e1e --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_async.cs @@ -0,0 +1,109 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + subscription = new( + name: new SubscriptionName(queueName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + FindQueueBy= QueueFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(queueName), + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs new file mode 100644 index 0000000000..7cbee17699 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByUrlTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByUrlTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + var queueUrl = FindQueueUrl(awsConnection, routingKey.ToValidSQSQueueName(true)).Result; + + subscription = new( + name: new SubscriptionName(queueName), + channelName: channel.Name, + routingKey: routingKey, + findQueueBy: QueueFindBy.Url, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + Topic = routingKey, + QueueUrl = queueUrl, + FindQueueBy = QueueFindBy.Url, + MakeChannels = OnMissingChannel.Validate, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + private static async Task FindQueueUrl(AWSMessagingGatewayConnection connection, string queueName) + { + using var snsClient = new AWSClientFactory(connection).CreateSqsClient(); + var topicResponse = await snsClient.GetQueueUrlAsync(queueName); + return topicResponse.QueueUrl; + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..2a3f90765b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,127 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _queueName; + private readonly string _messageGroupId; + private readonly string _deduplicationId; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + _messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + _deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + _queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(_queueName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + rawMessageDelivery: true, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType, partitionKey: _messageGroupId) + { + Bag = { [HeaderNames.DeduplicationId] = _deduplicationId } + }, + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + Topic = new RoutingKey(_queueName), + MakeChannels = OnMissingChannel.Create, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer_async() + { + // arrange + _message.Header.Subject = "test subject"; + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_queueName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + + message.Header.PartitionKey.Should().Be(_messageGroupId); + message.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + message.Header.Bag[HeaderNames.DeduplicationId].Should().Be(_deduplicationId); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_assume_throws_async.cs new file mode 100644 index 0000000000..ceec7ce2bb --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_assume_throws_async.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageConsumerAsync _consumer; + + public AWSAssumeQueuesTestsAsync() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Assume, + messagePumpType: MessagePumpType.Proactor, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _ = _channelFactory.CreateAsyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_queues_missing_assume_throws_async() + { + //we will try to get the queue url, and fail because it does not exist + await Assert.ThrowsAsync(async () => + await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_verify_throws_async.cs new file mode 100644 index 0000000000..c1a5400917 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_queues_missing_verify_throws_async.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTestsAsync : IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTestsAsync() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _awsConnection = GatewayFactory.CreateFactory(); + } + + [Fact] + public async Task When_queues_missing_verify_throws_async() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + await Assert.ThrowsAsync(async () => + await _channelFactory.CreateAsyncChannelAsync(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs new file mode 100644 index 0000000000..fd9715be16 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_raw_message_delivery_disabled_async.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsRawMessageDeliveryTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly IAmAChannelAsync _channel; + private readonly RoutingKey _routingKey; + + public SqsRawMessageDeliveryTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var queueName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(queueName); + + const int bufferSize = 10; + + // Set rawMessageDelivery to false + _channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: _routingKey, + bufferSize: bufferSize, + makeChannels: OnMissingChannel.Create, + rawMessageDelivery: true, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint)); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_raw_message_delivery_disabled_async() + { + // Arrange + var messageGroupId = $"MessageGroupId{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + var messageHeader = new MessageHeader( + Guid.NewGuid().ToString(), + _routingKey, + MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), + replyTo: RoutingKey.Empty, + contentType: "text\\plain", + partitionKey: messageGroupId) { Bag = { [HeaderNames.DeduplicationId] = deduplicationId } }; + + var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); + messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); + + var messageToSend = new Message(messageHeader, new MessageBody("test content one")); + + // Act + await _messageProducer.SendAsync(messageToSend); + + var messageReceived = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + await _channel.AcknowledgeAsync(messageReceived); + + // Assert + messageReceived.Id.Should().Be(messageToSend.Id); + messageReceived.Header.Topic.Should().Be(messageToSend.Header.Topic.ToValidSNSTopicName(true)); + messageReceived.Header.MessageType.Should().Be(messageToSend.Header.MessageType); + messageReceived.Header.CorrelationId.Should().Be(messageToSend.Header.CorrelationId); + messageReceived.Header.ReplyTo.Should().Be(messageToSend.Header.ReplyTo); + messageReceived.Header.ContentType.Should().Be(messageToSend.Header.ContentType); + messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); + messageReceived.Body.Value.Should().Be(messageToSend.Body.Value); + messageReceived.Header.PartitionKey.Should().Be(messageGroupId); + messageReceived.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + messageReceived.Header.Bag[HeaderNames.DeduplicationId].Should().Be(deduplicationId); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs new file mode 100644 index 0000000000..ba3b111d1c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var queueName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_rejecting_a_message_through_gateway_with_requeue_async() + { + await _messageProducer.SendAsync(_message); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.RejectAsync(message); + + // Let the timeout change + await Task.Delay(TimeSpan.FromMilliseconds(3000)); + + // should requeue_the_message + message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_a_message_async.cs new file mode 100644 index 0000000000..bf3aacd4b9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_a_message_async.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subcriptionName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subcriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await _sender.SendAsync(_message); + _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(_receivedMessage); + + _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.AcknowledgeAsync(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs new file mode 100644 index 0000000000..a2f008a575 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_requeueing_redrives_to_the_dlq_async.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _sender; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2), + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(_awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_redrives_to_the_queue_async() + { + await _sender.SendAsync(_message); + var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName.ToValidSQSQueueName(true)); + dlqCount.Should().Be(1); + } + + private async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = ["All", "ApproximateReceiveCount"] + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs new file mode 100644 index 0000000000..6e1197f6f4 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_throwing_defer_action_respect_redrive_async.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTestsAsync() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2), + channelType: ChannelType.PointToPoint + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer( + _awsConnection, + new SqsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Proactor(commandProcessor, messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, + TimeOut = TimeSpan.FromMilliseconds(5000), + RequeueCount = 3 + }; + } + + public async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, + MessageAttributeNames = new List { "All" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public async Task When_throwing_defer_action_respect_redrive_async() + { + await _sender.SendAsync(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + await Task.Delay(5000); + + var dlqCount = await GetDLQCountAsync(_dlqChannelName + ".fifo"); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_topic_missing_verify_throws_async.cs new file mode 100644 index 0000000000..8355763476 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Proactor/When_topic_missing_verify_throws_async.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTestsAsync +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTestsAsync() + { + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(queueName); + + _awsConnection = GatewayFactory.CreateFactory(); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public async Task When_topic_missing_verify_throws_async() + { + // arrange + var producer = new SqsMessageProducer(_awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Validate, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + + // act & assert + await Assert.ThrowsAsync(async () => + await producer.SendAsync(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, + type: "plain/text", partitionKey: messageGroupId), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs new file mode 100644 index 0000000000..22769410a5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_a_message_consumer_reads_multiple_messages.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + _channelFactory = new ChannelFactory(awsConnection); + + var subscriptionName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + contentBasedDeduplication: true, + deduplicationScope: DeduplicationScope.MessageGroup, + fifoThroughputLimit: 1, + channelType: ChannelType.PointToPoint + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages() + { + var routingKey = new RoutingKey(_queueName); + + var messageGroupIdOne = $"MessageGroup{Guid.NewGuid():N}"; + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdOne), + new MessageBody("test content two") + ); + + + var messageGroupIdTwo = $"MessageGroup{Guid.NewGuid():N}"; + var deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo) + { + Bag = { [HeaderNames.DeduplicationId] = deduplicationId } + }, + new MessageBody("test content four") + ); + + var messageFive = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType, partitionKey: messageGroupIdTwo), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + _messageProducer.Send(messageOne); + _messageProducer.Send(messageTwo); + _messageProducer.Send(messageThree); + _messageProducer.Send(messageFour); + _messageProducer.Send(messageFive); + + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + _consumer.Acknowledge(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + + messagesReceivedCount.Should().Be(4); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageProducerAsync)_messageProducer).DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_assume.cs new file mode 100644 index 0000000000..18ef09c53c --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_assume.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(queueName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Assume, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(true)); + } + + [Fact] + public void When_infastructure_exists_can_assume() + { + //arrange + _messageProducer.Send(_message); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify.cs new file mode 100644 index 0000000000..a028e346ab --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + FindQueueBy = QueueFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(queueName), + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify_by_url.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify_by_url.cs new file mode 100644 index 0000000000..6e38749717 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_infastructure_exists_can_verify_by_url.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByUrlTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByUrlTests () + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _myCommand = new MyCommand { Value = "Test" }; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + var queueUrl = FindQueueUrl(awsConnection, routingKey.ToValidSQSQueueName(true)); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Arn, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + Topic = routingKey, + QueueUrl = queueUrl, + FindQueueBy = QueueFindBy.Url, + MakeChannels = OnMissingChannel.Validate, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + + private static string FindQueueUrl(AWSMessagingGatewayConnection connection, string queueName) + { + using var snsClient = new AWSClientFactory(connection).CreateSqsClient(); + var topicResponse = snsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + return topicResponse.QueueUrl; + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs new file mode 100644 index 0000000000..db1956fe59 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_posting_a_message_via_the_messaging_gateway.cs @@ -0,0 +1,128 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _queueName; + private readonly string _messageGroupId; + private readonly string _deduplicationId; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + _messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + _deduplicationId = $"DeduplicationId{Guid.NewGuid():N}"; + _queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(_queueName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + rawMessageDelivery: true, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType, partitionKey: _messageGroupId) + { + Bag = { [HeaderNames.DeduplicationId] = _deduplicationId } + }, + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + Topic = new RoutingKey(_queueName), + MakeChannels = OnMissingChannel.Create, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public void When_posting_a_message_via_the_producer() + { + // arrange + _message.Header.Subject = "test subject"; + _messageProducer.Send(_message); + + Task.Delay(1000).GetAwaiter().GetResult(); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + _channel.Acknowledge(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_queueName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + + message.Header.PartitionKey.Should().Be(_messageGroupId); + message.Header.Bag.Should().ContainKey(HeaderNames.DeduplicationId); + message.Header.Bag[HeaderNames.DeduplicationId].Should().Be(_deduplicationId); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} + diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_assume_throws.cs new file mode 100644 index 0000000000..a3c051278f --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_assume_throws.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly SqsMessageConsumer _consumer; + + public AWSAssumeQueuesTests() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume, + sqsType: SnsSqsType.Fifo + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + producer.ConfirmTopicExistsAsync(topicName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_queues_missing_assume_throws() + { + //we will try to get the queue url, and fail because it does not exist + Assert.Throws(() => _consumer.Receive(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_verify_throws.cs new file mode 100644 index 0000000000..2a36c8afe0 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_queues_missing_verify_throws.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTests : IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTests() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _awsConnection = GatewayFactory.CreateFactory(); + } + + [Fact] + public void When_queues_missing_verify_throws() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + Assert.Throws(() => _channelFactory.CreateAsyncChannel(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs new file mode 100644 index 0000000000..99e6c67c80 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTests : IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var queueName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(queueName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + } + + [Fact] + public void When_rejecting_a_message_through_gateway_with_requeue() + { + _messageProducer.Send(_message); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + _channel.Reject(message); + + // Let the timeout change + Task.Delay(TimeSpan.FromMilliseconds(3000)).GetAwaiter().GetResult(); + + // should requeue_the_message + message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + _channel.Acknowledge(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_a_message.cs new file mode 100644 index 0000000000..812de8f1a0 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_a_message.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerSync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + sqsType: SnsSqsType.Fifo, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create, SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_a_message() + { + _sender.Send(_message); + _receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(_receivedMessage); + + _requeuedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + _channel.Acknowledge(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs new file mode 100644 index 0000000000..3add76a4c5 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_requeueing_redrives_to_the_dlq.cs @@ -0,0 +1,119 @@ +using System; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable +{ + private readonly SnsMessageProducer _sender; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(topicName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2), + sqsType: SnsSqsType.Fifo + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(_awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create, SnsAttributes = new SnsAttributes { Type = SnsSqsType.Fifo } + }); + + _sender.ConfirmTopicExistsAsync(topicName).Wait(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_redrives_to_the_queue() + { + _sender.Send(_message); + var receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + //should force us into the dlq + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName + ".fifo").Should().Be(1); + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = ["All", "ApproximateReceiveCount"] + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs new file mode 100644 index 0000000000..f59fa3b9b9 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_throwing_defer_action_respect_redrive.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2), + channelType: ChannelType.PointToPoint + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType, partitionKey: messageGroupId), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer( + _awsConnection, + new SqsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Reactor(commandProcessor, messageMapperRegistry, + new EmptyMessageTransformerFactory(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + public int GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = ["ApproximateReceiveCount"], + MessageAttributeNames = new List { "All" } + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public void When_throwing_defer_action_respect_redrive_async() + { + _sender.Send(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + Task.Delay(5000).GetAwaiter().GetResult(); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + Task.WaitAll(task); + + Task.Delay(5000).GetAwaiter().GetResult(); + + var dlqCount = GetDLQCountAsync(_dlqChannelName + ".fifo"); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_topic_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_topic_missing_verify_throws.cs new file mode 100644 index 0000000000..8bf9cf6d6e --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Fifo/Reactor/When_topic_missing_verify_throws.cs @@ -0,0 +1,45 @@ +using System; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Fifo.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTests +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTests() + { + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(queueName); + + _awsConnection = GatewayFactory.CreateFactory(); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public void When_topic_missing_verify_throws() + { + // arrange + var producer = new SqsMessageProducer(_awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Validate, + SqsAttributes = new SqsAttributes { Type = SnsSqsType.Fifo } + }); + + var messageGroupId = $"MessageGroup{Guid.NewGuid():N}"; + + // act & assert + Assert.Throws(() => + producer.Send(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, + type: "plain/text", partitionKey: messageGroupId), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs new file mode 100644 index 0000000000..a0ab410623 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_a_message_consumer_reads_multiple_messages_async.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTestsAsync() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var subscriptionName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )).GetAwaiter().GetResult(); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages_async() + { + var routingKey = new RoutingKey(_queueName); + + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var messageTwo = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content two") + ); + + var messageThree = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content three") + ); + + var messageFour = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + await _messageProducer.SendAsync(messageOne); + await _messageProducer.SendAsync(messageTwo); + await _messageProducer.SendAsync(messageThree); + await _messageProducer.SendAsync(messageFour); + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + await _consumer.AcknowledgeAsync(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + messagesReceivedCount.Should().Be(4); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); + _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); + _messageProducer.DisposeAsync().GetAwaiter().GetResult(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_customising_aws_client_config_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_customising_aws_client_config_async.cs new file mode 100644 index 0000000000..2503dc24fe --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_customising_aws_client_config_async.cs @@ -0,0 +1,96 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class CustomisingAwsClientConfigTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + + public CustomisingAwsClientConfigTestsAsync() + { + MyCommand myCommand = new() { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + string correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + messagePumpType: MessagePumpType.Proactor, + routingKey: routingKey, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var subscribeAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = + new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sqs_async_sub")); + }); + + _channelFactory = new ChannelFactory(subscribeAwsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + var publishAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = + new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sqs_async_pub")); + }); + + _messageProducer = new SqsMessageProducer(publishAwsConnection, + new SqsPublication { Topic = new RoutingKey(queueName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_customising_aws_client_config() + { + //arrange + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + await _channel.AcknowledgeAsync(message); + + //publish_and_subscribe_should_use_custom_http_client_factory + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("async_sub"); + InterceptingDelegatingHandler.RequestCount["async_sub"].Should().BeGreaterThan(0); + + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("async_pub"); + InterceptingDelegatingHandler.RequestCount["async_pub"].Should().BeGreaterThan(0); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_assume_async.cs new file mode 100644 index 0000000000..acac11ba89 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_assume_async.cs @@ -0,0 +1,98 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTestsAsync() + { + _myCommand = new MyCommand{Value = "Test"}; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Assume + ); + + _messageProducer = new SqsMessageProducer(awsConnection, new SqsPublication{MakeChannels = OnMissingChannel.Assume}); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public async Task When_infastructure_exists_can_assume() + { + //arrange + await _messageProducer.SendAsync(_message); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + await _consumer.AcknowledgeAsync(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_verify_by_url.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_verify_by_url.cs new file mode 100644 index 0000000000..1773f0c639 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infastructure_exists_can_verify_by_url.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByUrlTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByUrlTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + SqsSubscription subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + var queueUrl = FindQueueUrl(awsConnection, routingKey.Value); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + findQueueBy: QueueFindBy.Url, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + Topic = routingKey, + QueueUrl= queueUrl, + FindQueueBy = QueueFindBy.Url, + MakeChannels = OnMissingChannel.Validate + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } + + private static string FindQueueUrl(AWSMessagingGatewayConnection connection, string queueName) + { + using var snsClient = new AWSClientFactory(connection).CreateSqsClient(); + var topicResponse = snsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + return topicResponse.QueueUrl; + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs similarity index 66% rename from tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs rename to tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs index 2d07a58097..4f04418877 100644 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_convention.cs +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_async.cs @@ -2,19 +2,17 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; using FluentAssertions; using Paramore.Brighter.AWS.Tests.Helpers; using Paramore.Brighter.AWS.Tests.TestDoubles; using Paramore.Brighter.MessagingGateway.AWSSQS; using Xunit; -namespace Paramore.Brighter.AWS.Tests.MessagingGateway +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor { [Trait("Category", "AWS")] [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByConventionTestsAsync : IAsyncDisposable, IDisposable + public class AWSValidateInfrastructureTestsAsync : IDisposable, IAsyncDisposable { private readonly Message _message; private readonly IAmAMessageConsumerAsync _consumer; @@ -22,22 +20,23 @@ public class AWSValidateInfrastructureByConventionTestsAsync : IAsyncDisposable, private readonly ChannelFactory _channelFactory; private readonly MyCommand _myCommand; - public AWSValidateInfrastructureByConventionTestsAsync() + public AWSValidateInfrastructureTestsAsync() { _myCommand = new MyCommand { Value = "Test" }; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), routingKey: routingKey, messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Create + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint ); _message = new Message( @@ -46,27 +45,28 @@ public AWSValidateInfrastructureByConventionTestsAsync() new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) ); - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); + var awsConnection = GatewayFactory.CreateFactory(); _channelFactory = new ChannelFactory(awsConnection); var channel = _channelFactory.CreateAsyncChannel(subscription); subscription = new( - name: new SubscriptionName(channelName), + name: new SubscriptionName(subscriptionName), channelName: channel.Name, routingKey: routingKey, - findTopicBy: TopicFindBy.Convention, + findTopicBy: TopicFindBy.Name, messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Validate + makeChannels: OnMissingChannel.Validate, + channelType: ChannelType.PointToPoint ); _messageProducer = new SqsMessageProducer( awsConnection, - new SnsPublication + new SqsPublication { - FindTopicBy = TopicFindBy.Convention, - MakeChannels = OnMissingChannel.Validate + FindQueueBy = QueueFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(queueName) } ); @@ -87,7 +87,7 @@ public async Task When_infrastructure_exists_can_verify_async() await _consumer.AcknowledgeAsync(message); } - + public void Dispose() { //Clean up resources that we have created diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs new file mode 100644 index 0000000000..6cc9c67b39 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_infrastructure_exists_can_verify_by_url_async.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureByUrlTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerAsync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureByUrlTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + var queueUrl = FindQueueUrl(awsConnection, routingKey.Value).Result; + + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Arn, + makeChannels: OnMissingChannel.Validate + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + Topic = routingKey, + QueueUrl = queueUrl, + FindQueueBy = QueueFindBy.Url, + MakeChannels = OnMissingChannel.Validate + }); + + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify_async() + { + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + await _consumer.AcknowledgeAsync(message); + } + + private static async Task FindQueueUrl(AWSMessagingGatewayConnection connection, string queueName) + { + using var snsClient = new AWSClientFactory(connection).CreateSqsClient(); + var response = await snsClient.GetQueueUrlAsync(queueName); + return response.QueueUrl; + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + ((IAmAMessageConsumerSync)_consumer).Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _consumer.DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs new file mode 100644 index 0000000000..af0d7d660b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_posting_a_message_via_the_messaging_gateway_async.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _queueName; + + public SqsMessageProducerSendAsyncTests() + { + _myCommand = new MyCommand { Value = "Test" }; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, new SqsPublication { Topic = new RoutingKey(_queueName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer_async() + { + // arrange + _message.Header.Subject = "test subject"; + await _messageProducer.SendAsync(_message); + + await Task.Delay(1000); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + // should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_queueName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + // allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_assume_throws_async.cs new file mode 100644 index 0000000000..13715e4d37 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_assume_throws_async.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly IAmAMessageConsumerAsync _consumer; + + public AWSAssumeQueuesTestsAsync() + { + var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Assume, + messagePumpType: MessagePumpType.Proactor, + channelType: ChannelType.PointToPoint + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //create the topic, we want the queue to be the issue + //We need to create the topic at least, to check the queues + var producer = new SnsMessageProducer(awsConnection, + new SnsPublication + { + MakeChannels = OnMissingChannel.Create + }); + + producer.ConfirmTopicExistsAsync(queueName).Wait(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateAsyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); + } + + [Fact] + public async Task When_queues_missing_assume_throws_async() + { + //we will try to get the queue url, and fail because it does not exist + await Assert.ThrowsAsync(async () => await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_verify_throws_async.cs new file mode 100644 index 0000000000..c5e4f55296 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_queues_missing_verify_throws_async.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTestsAsync : IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTestsAsync() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + makeChannels: OnMissingChannel.Validate, + channelType: ChannelType.PointToPoint + ); + + _awsConnection = GatewayFactory.CreateFactory(); + } + + [Fact] + public async Task When_queues_missing_verify_throws_async() + { + // We have no queues so we should throw + // We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + await Assert.ThrowsAsync(async () => await _channelFactory.CreateAsyncChannelAsync(_subscription)); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs new file mode 100644 index 0000000000..4b2ed34a43 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_rejecting_a_message_through_gateway_with_requeue_async.cs @@ -0,0 +1,88 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTestsAsync() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + + _messageProducer = + new SqsMessageProducer(awsConnection, new SqsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_rejecting_a_message_through_gateway_with_requeue_async() + { + await _messageProducer.SendAsync(_message); + + var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.RejectAsync(message); + + // Let the timeout change + await Task.Delay(TimeSpan.FromMilliseconds(3000)); + + // should requeue_the_message + message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + // clear the queue + await _channel.AcknowledgeAsync(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_a_message_async.cs new file mode 100644 index 0000000000..3a0acb74f1 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_a_message_async.cs @@ -0,0 +1,83 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Runtime.CredentialManagement; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerAsync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(channelName), + channelName: new ChannelName(channelName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + new CredentialProfileStoreChain(); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SnsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_a_message_async() + { + await _sender.SendAsync(_message); + _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(_receivedMessage); + + _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + + await _channel.AcknowledgeAsync(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs new file mode 100644 index 0000000000..600b09c73a --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_requeueing_redrives_to_the_dlq_async.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _sender; + private readonly IAmAChannelAsync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTestsAsync() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2), + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(_awsConnection, new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(subscription); + } + + [Fact] + public async Task When_requeueing_redrives_to_the_queue_async() + { + await _sender.SendAsync(_message); + var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); + await _channel.RequeueAsync(receivedMessage); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + private async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = ["All", "ApproximateReceiveCount"] + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs new file mode 100644 index 0000000000..4a1789a4c6 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_throwing_defer_action_respect_redrive_async.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelAsync _channel; + private readonly SqsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTestsAsync() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + requeueCount: -1, + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Proactor, + redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2), + channelType: ChannelType.PointToPoint + ); + + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer( + _awsConnection, + new SqsPublication + { + Topic = routingKey, + RequestType = typeof(MyDeferredCommand), + MakeChannels = OnMissingChannel.Create + } + ); + + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateAsyncChannel(_subscription); + + IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); + + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.RegisterAsync(); + + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactoryAsync(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + _messagePump = new Proactor(commandProcessor, messageMapperRegistry, + new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + private async Task GetDLQCountAsync(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); + var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, + MessageAttributeNames = new List { "All" } + }); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + [Fact(Skip = "Failing async tests caused by task scheduler issues")] + public async Task When_throwing_defer_action_respect_redrive_async() + { + await _sender.SendAsync(_message); + + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + await Task.WhenAll(task); + + await Task.Delay(5000); + + int dlqCount = await GetDLQCountAsync(_dlqChannelName); + dlqCount.Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_topic_missing_verify_throws_async.cs new file mode 100644 index 0000000000..fd0c7cb261 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Proactor/When_topic_missing_verify_throws_async.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Proactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTestsAsync +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTestsAsync() + { + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(queueName); + + _awsConnection = GatewayFactory.CreateFactory(); + + // Because we don't use channel factory to create the infrastructure - it won't exist + } + + [Fact] + public async Task When_topic_missing_verify_throws_async() + { + // arrange + var producer = new SqsMessageProducer(_awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Validate + }); + + // act & assert + await Assert.ThrowsAsync(async () => + await producer.SendAsync(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs new file mode 100644 index 0000000000..33fb5dc3fe --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_a_message_consumer_reads_multiple_messages.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _messageProducer; + private readonly SqsMessageConsumer _consumer; + private readonly string _queueName; + private readonly ChannelFactory _channelFactory; + private const string ContentType = "text\\plain"; + private const int BufferSize = 3; + private const int MessageCount = 4; + + public SQSBufferedConsumerTests() + { + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _queueName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var subscriptionName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + //we need the channel to create the queues and notifications + var routingKey = new RoutingKey(_queueName); + + var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName:new ChannelName(_queueName), + routingKey:routingKey, + bufferSize: BufferSize, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + )); + + //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel + //just for the tests, so create a new consumer from the properties + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Create + }); + } + + [Fact] + public async Task When_a_message_consumer_reads_multiple_messages() + { + var routingKey = new RoutingKey(_queueName); + + var messageOne = new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content one") + ); + + var messageTwo= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content two") + ); + + var messageThree= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content three") + ); + + var messageFour= new Message( + new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, + correlationId: Guid.NewGuid().ToString(), contentType: ContentType), + new MessageBody("test content four") + ); + + //send MESSAGE_COUNT messages + _messageProducer.Send(messageOne); + _messageProducer.Send(messageTwo); + _messageProducer.Send(messageThree); + _messageProducer.Send(messageFour); + + + int iteration = 0; + var messagesReceived = new List(); + var messagesReceivedCount = messagesReceived.Count; + do + { + iteration++; + var outstandingMessageCount = MessageCount - messagesReceivedCount; + + //retrieve messages + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); + + messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); + + //should not receive more than buffer in one hit + messages.Length.Should().BeLessOrEqualTo(BufferSize); + + var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); + foreach (var message in moreMessages) + { + messagesReceived.Add(message); + _consumer.Acknowledge(message); + } + + messagesReceivedCount = messagesReceived.Count; + + await Task.Delay(1000); + + } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); + + + messagesReceivedCount.Should().Be(4); + + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageProducerAsync) _messageProducer).DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_customising_aws_client_config.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_customising_aws_client_config.cs new file mode 100644 index 0000000000..b19d3f0331 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_customising_aws_client_config.cs @@ -0,0 +1,94 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class CustomisingAwsClientConfigTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + + public CustomisingAwsClientConfigTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + MyCommand myCommand = new() { Value = "Test" }; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + messagePumpType: MessagePumpType.Reactor, + routingKey: routingKey, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + var subscribeAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sqs_sync_sub")); + }); + + _channelFactory = new ChannelFactory(subscribeAwsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + var publishAwsConnection = GatewayFactory.CreateFactory(config => + { + config.HttpClientFactory = new InterceptingHttpClientFactory(new InterceptingDelegatingHandler("sqs_sync_pub")); + }); + + _messageProducer = new SqsMessageProducer(publishAwsConnection, + new SqsPublication { Topic = new RoutingKey(queueName), MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public async Task When_customising_aws_client_config() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + //publish_and_subscribe_should_use_custom_http_client_factory + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("sqs_sync_sub"); + InterceptingDelegatingHandler.RequestCount["sqs_sync_sub"].Should().BeGreaterThan(0); + + InterceptingDelegatingHandler.RequestCount.Should().ContainKey("sqs_sync_pub"); + InterceptingDelegatingHandler.RequestCount["sqs_sync_pub"].Should().BeGreaterThan(0); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_assume.cs new file mode 100644 index 0000000000..f1ffd72f73 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_assume.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly SqsMessageConsumer _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSAssumeInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + channelType: ChannelType.PointToPoint, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume + ); + + _messageProducer = new SqsMessageProducer(awsConnection, + new SqsPublication { MakeChannels = OnMissingChannel.Assume }); + + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_infastructure_exists_can_assume() + { + //arrange + _messageProducer.Send(_message); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_verify.cs new file mode 100644 index 0000000000..fda5e3d035 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_infastructure_exists_can_verify.cs @@ -0,0 +1,115 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAMessageConsumerSync _consumer; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public AWSValidateInfrastructureTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Create, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + //This doesn't look that different from our create tests - this is because we create using the channel factory in + //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //Now change the subscription to validate, just check what we made + subscription = new( + name: new SubscriptionName(subscriptionName), + channelName: channel.Name, + routingKey: routingKey, + findTopicBy: TopicFindBy.Name, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + channelType: ChannelType.PointToPoint + ); + + _messageProducer = new SqsMessageProducer( + awsConnection, + new SqsPublication + { + FindQueueBy = QueueFindBy.Name, + MakeChannels = OnMissingChannel.Validate, + Topic = new RoutingKey(queueName) + } + ); + + _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); + } + + [Fact] + public async Task When_infrastructure_exists_can_verify() + { + //arrange + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); + + //Assert + var message = messages.First(); + message.Id.Should().Be(_myCommand.Id); + + //clear the queue + _consumer.Acknowledge(message); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _consumer.Dispose(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); + await _messageProducer.DisposeAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs new file mode 100644 index 0000000000..0d7ee2cf97 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_posting_a_message_via_the_messaging_gateway.cs @@ -0,0 +1,111 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerSendTests : IDisposable, IAsyncDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + private readonly string _correlationId; + private readonly string _replyTo; + private readonly string _contentType; + private readonly string _queueName; + + public SqsMessageProducerSendTests() + { + _myCommand = new MyCommand{Value = "Test"}; + _correlationId = Guid.NewGuid().ToString(); + _replyTo = "http:\\queueUrl"; + _contentType = "text\\plain"; + _queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(_queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(_queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, + replyTo: new RoutingKey(_replyTo), contentType: _contentType), + new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = new SqsMessageProducer(awsConnection, new SqsPublication{Topic = new RoutingKey(_queueName), MakeChannels = OnMissingChannel.Create}); + } + + [Fact] + public async Task When_posting_a_message_via_the_producer() + { + //arrange + _message.Header.Subject = "test subject"; + _messageProducer.Send(_message); + + await Task.Delay(1000); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + //should_send_the_message_to_aws_sqs + message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); + + message.Id.Should().Be(_myCommand.Id); + message.Redelivered.Should().BeFalse(); + message.Header.MessageId.Should().Be(_myCommand.Id); + message.Header.Topic.Value.Should().Contain(_queueName); + message.Header.CorrelationId.Should().Be(_correlationId); + message.Header.ReplyTo.Should().Be(_replyTo); + message.Header.ContentType.Should().Be(_contentType); + message.Header.HandledCount.Should().Be(0); + message.Header.Subject.Should().Be(_message.Header.Subject); + //allow for clock drift in the following test, more important to have a contemporary timestamp than anything + message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); + message.Header.Delayed.Should().Be(TimeSpan.Zero); + //{"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} + message.Body.Value.Should().Be(_message.Body.Value); + } + + public void Dispose() + { + //Clean up resources that we have created + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + _messageProducer.Dispose(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + await _messageProducer.DisposeAsync(); + } + + private static DateTime RoundToSeconds(DateTime dateTime) + { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } + +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_assume_throws.cs new file mode 100644 index 0000000000..03d4d60f88 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_assume_throws.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly ChannelFactory _channelFactory; + private readonly SqsMessageConsumer _consumer; + + public AWSAssumeQueuesTests() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Assume, + channelType: ChannelType.PointToPoint + ); + + var awsConnection = GatewayFactory.CreateFactory(); + + _channelFactory = new ChannelFactory(awsConnection); + var channel = _channelFactory.CreateSyncChannel(subscription); + + //We need to create the topic at least, to check the queues + _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); + } + + [Fact] + public void When_queues_missing_assume_throws() + { + //we will try to get the queue url, and fail because it does not exist + Assert.Throws(() => _consumer.Receive(TimeSpan.FromMilliseconds(1000))); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } + +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_verify_throws.cs new file mode 100644 index 0000000000..2f28f078a7 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_queues_missing_verify_throws.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateQueuesTests : IDisposable, IAsyncDisposable +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private ChannelFactory _channelFactory; + + public AWSValidateQueuesTests() + { + var subscriptionName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + messagePumpType: MessagePumpType.Reactor, + makeChannels: OnMissingChannel.Validate, + channelType: ChannelType.PointToPoint + ); + + _awsConnection = GatewayFactory.CreateFactory(); + } + + [Fact] + public void When_queues_missing_verify_throws() + { + //We have no queues so we should throw + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + Assert.Throws(() => _channelFactory.CreateSyncChannel(_subscription)); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs new file mode 100644 index 0000000000..513eceb157 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_rejecting_a_message_through_gateway_with_requeue.cs @@ -0,0 +1,89 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageConsumerRequeueTests : IDisposable +{ + private readonly Message _message; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _messageProducer; + private readonly ChannelFactory _channelFactory; + private readonly MyCommand _myCommand; + + public SqsMessageConsumerRequeueTests() + { + _myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + messagePumpType: MessagePumpType.Reactor, + routingKey: routingKey, + channelType: ChannelType.PointToPoint + ); + + _message = new Message( + new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + var awsConnection = GatewayFactory.CreateFactory(); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + + _messageProducer = + new SqsMessageProducer(awsConnection, new SqsPublication { MakeChannels = OnMissingChannel.Create }); + } + + [Fact] + public void When_rejecting_a_message_through_gateway_with_requeue() + { + _messageProducer.Send(_message); + + var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + _channel.Reject(message); + + //Let the timeout change + Task.Delay(TimeSpan.FromMilliseconds(3000)); + + //should requeue_the_message + message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(message); + + message.Id.Should().Be(_myCommand.Id); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_a_message.cs new file mode 100644 index 0000000000..20fd2f15d6 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_a_message.cs @@ -0,0 +1,83 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.Runtime.CredentialManagement; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessageProducerSync _sender; + private Message _requeuedMessage; + private Message _receivedMessage; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + + public SqsMessageProducerRequeueTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + new CredentialProfileStoreChain(); + + var awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(awsConnection, new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_a_message() + { + _sender.Send(_message); + _receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(_receivedMessage); + + _requeuedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + + //clear the queue + _channel.Acknowledge(_requeuedMessage); + + _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs new file mode 100644 index 0000000000..484d77b894 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_requeueing_redrives_to_the_dlq.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable +{ + private readonly SqsMessageProducer _sender; + private readonly IAmAChannelSync _channel; + private readonly ChannelFactory _channelFactory; + private readonly Message _message; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly string _dlqChannelName; + + public SqsMessageProducerDlqTests() + { + MyCommand myCommand = new MyCommand { Value = "Test" }; + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + var subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) + ); + + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + _sender = new SqsMessageProducer(_awsConnection, new SqsPublication { MakeChannels = OnMissingChannel.Create }); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(subscription); + } + + [Fact] + public void When_requeueing_redrives_to_the_queue() + { + _sender.Send(_message); + var receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + //should force us into the dlq + receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); + _channel.Requeue(receivedMessage); + + Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName).Should().Be(1); + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs new file mode 100644 index 0000000000..b06a619019 --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_throwing_defer_action_respect_redrive.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using Amazon.SQS; +using Amazon.SQS.Model; +using FluentAssertions; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.AWS.Tests.TestDoubles; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Paramore.Brighter.ServiceActivator; +using Polly.Registry; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +[Trait("Fragile", "CI")] +public class SnsReDrivePolicySDlqTests : IDisposable, IAsyncDisposable +{ + private readonly IAmAMessagePump _messagePump; + private readonly Message _message; + private readonly string _dlqChannelName; + private readonly IAmAChannelSync _channel; + private readonly SqsMessageProducer _sender; + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly SqsSubscription _subscription; + private readonly ChannelFactory _channelFactory; + + public SnsReDrivePolicySDlqTests() + { + const string replyTo = "http:\\queueUrl"; + const string contentType = "text\\plain"; + + _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var correlationId = Guid.NewGuid().ToString(); + var subscriptionName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var queueName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + var routingKey = new RoutingKey(queueName); + + //how are we consuming + _subscription = new SqsSubscription( + name: new SubscriptionName(subscriptionName), + channelName: new ChannelName(queueName), + routingKey: routingKey, + channelType: ChannelType.PointToPoint, + //don't block the redrive policy from owning retry management + requeueCount: -1, + //delay before requeuing + requeueDelay: TimeSpan.FromMilliseconds(50), + messagePumpType: MessagePumpType.Reactor, + //we want our SNS subscription to manage requeue limits using the DLQ for 'too many requeues' + redrivePolicy: new RedrivePolicy + ( + deadLetterQueueName: new ChannelName(_dlqChannelName), + maxReceiveCount: 2 + )); + + //what do we send + var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; + _message = new Message( + new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, + replyTo: new RoutingKey(replyTo), contentType: contentType), + new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) + ); + + //Must have credentials stored in the SDK Credentials store or shared credentials file + _awsConnection = GatewayFactory.CreateFactory(); + + //how do we send to the queue + _sender = new SqsMessageProducer( + _awsConnection, + new SqsPublication + { + Topic = routingKey, RequestType = typeof(MyDeferredCommand), MakeChannels = OnMissingChannel.Create + } + ); + + //We need to do this manually in a test - will create the channel from subscriber parameters + _channelFactory = new ChannelFactory(_awsConnection); + _channel = _channelFactory.CreateSyncChannel(_subscription); + + //how do we handle a command + IHandleRequests handler = new MyDeferredCommandHandler(); + + //hook up routing for the command processor + var subscriberRegistry = new SubscriberRegistry(); + subscriberRegistry.Register(); + + //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here + IAmACommandProcessor commandProcessor = new CommandProcessor( + subscriberRegistry: subscriberRegistry, + handlerFactory: new QuickHandlerFactory(() => handler), + requestContextFactory: new InMemoryRequestContextFactory(), + policyRegistry: new PolicyRegistry() + ); + + var messageMapperRegistry = new MessageMapperRegistry( + new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), + null + ); + messageMapperRegistry.Register(); + + //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test + _messagePump = new Reactor(commandProcessor, messageMapperRegistry, + null, new InMemoryRequestContextFactory(), _channel) + { + Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 + }; + } + + private int GetDLQCount(string queueName) + { + using var sqsClient = new AWSClientFactory(_awsConnection).CreateSqsClient(); + var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); + var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest + { + QueueUrl = queueUrlResponse.QueueUrl, + WaitTimeSeconds = 5, + MessageSystemAttributeNames = ["ApproximateReceiveCount"], + MessageAttributeNames = new List { "All" } + }).GetAwaiter().GetResult(); + + if (response.HttpStatusCode != HttpStatusCode.OK) + { + throw new AmazonSQSException( + $"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); + } + + return response.Messages.Count; + } + + + [Fact] + public async Task When_throwing_defer_action_respect_redrive() + { + //put something on an SNS topic, which will be delivered to our SQS queue + _sender.Send(_message); + + //start a message pump, let it process messages + var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); + await Task.Delay(5000); + + //send a quit message to the pump to terminate it + var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); + _channel.Enqueue(quitMessage); + + //wait for the pump to stop once it gets a quit message + await Task.WhenAll(task); + + await Task.Delay(5000); + + //inspect the dlq + GetDLQCount(_dlqChannelName).Should().Be(1); + } + + public void Dispose() + { + _channelFactory.DeleteTopicAsync().Wait(); + _channelFactory.DeleteQueueAsync().Wait(); + } + + public async ValueTask DisposeAsync() + { + await _channelFactory.DeleteTopicAsync(); + await _channelFactory.DeleteQueueAsync(); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_topic_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_topic_missing_verify_throws.cs new file mode 100644 index 0000000000..8c41e6566b --- /dev/null +++ b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/Sqs/Standard/Reactor/When_topic_missing_verify_throws.cs @@ -0,0 +1,40 @@ +using System; +using Amazon.SQS.Model; +using Paramore.Brighter.AWS.Tests.Helpers; +using Paramore.Brighter.MessagingGateway.AWSSQS; +using Xunit; + +namespace Paramore.Brighter.AWS.Tests.MessagingGateway.Sqs.Standard.Reactor; + +[Trait("Category", "AWS")] +public class AWSValidateMissingTopicTests +{ + private readonly AWSMessagingGatewayConnection _awsConnection; + private readonly RoutingKey _routingKey; + + public AWSValidateMissingTopicTests() + { + string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); + _routingKey = new RoutingKey(topicName); + + _awsConnection = GatewayFactory.CreateFactory(); + + //Because we don't use channel factory to create the infrastructure -it won't exist + } + + [Fact] + public void When_topic_missing_verify_throws() + { + //arrange + var producer = new SqsMessageProducer(_awsConnection, + new SqsPublication + { + MakeChannels = OnMissingChannel.Validate, + }); + + //act && assert + Assert.Throws(() => producer.Send(new Message( + new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), + new MessageBody("Test")))); + } +} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs deleted file mode 100644 index 26ffbd852d..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SQSBufferedConsumerTests : IDisposable, IAsyncDisposable - { - private readonly SqsMessageProducer _messageProducer; - private readonly SqsMessageConsumer _consumer; - private readonly string _topicName; - private readonly ChannelFactory _channelFactory; - private const string ContentType = "text\\plain"; - private const int BufferSize = 3; - private const int MessageCount = 4; - - public SQSBufferedConsumerTests() - { - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - - //we need the channel to create the queues and notifications - var routingKey = new RoutingKey(_topicName); - - var channel = _channelFactory.CreateSyncChannel(new SqsSubscription( - name: new SubscriptionName(channelName), - channelName:new ChannelName(channelName), - routingKey:routingKey, - bufferSize: BufferSize, - makeChannels: OnMissingChannel.Create - )); - - //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel - //just for the tests, so create a new consumer from the properties - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); - _messageProducer = new SqsMessageProducer(awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - } - - [Fact] - public async Task When_a_message_consumer_reads_multiple_messages() - { - var routingKey = new RoutingKey(_topicName); - - var messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content one") - ); - - var messageTwo= new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content two") - ); - - var messageThree= new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content three") - ); - - var messageFour= new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content four") - ); - - //send MESSAGE_COUNT messages - _messageProducer.Send(messageOne); - _messageProducer.Send(messageTwo); - _messageProducer.Send(messageThree); - _messageProducer.Send(messageFour); - - - int iteration = 0; - var messagesReceived = new List(); - var messagesReceivedCount = messagesReceived.Count; - do - { - iteration++; - var outstandingMessageCount = MessageCount - messagesReceivedCount; - - //retrieve messages - var messages = _consumer.Receive(TimeSpan.FromMilliseconds(10000)); - - messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); - - //should not receive more than buffer in one hit - messages.Length.Should().BeLessOrEqualTo(BufferSize); - - var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); - foreach (var message in moreMessages) - { - messagesReceived.Add(message); - _consumer.Acknowledge(message); - } - - messagesReceivedCount = messagesReceived.Count; - - await Task.Delay(1000); - - } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); - - - messagesReceivedCount.Should().Be(4); - - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await ((IAmAMessageProducerAsync) _messageProducer).DisposeAsync(); - } - } -} - - diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs deleted file mode 100644 index 3e06a5ecbe..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_a_message_consumer_reads_multiple_messages_async.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SQSBufferedConsumerTestsAsync : IDisposable, IAsyncDisposable - { - private readonly SqsMessageProducer _messageProducer; - private readonly SqsMessageConsumer _consumer; - private readonly string _topicName; - private readonly ChannelFactory _channelFactory; - private const string ContentType = "text\\plain"; - private const int BufferSize = 3; - private const int MessageCount = 4; - - public SQSBufferedConsumerTestsAsync() - { - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - var channelName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _topicName = $"Buffered-Consumer-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - - //we need the channel to create the queues and notifications - var routingKey = new RoutingKey(_topicName); - - var channel = _channelFactory.CreateAsyncChannelAsync(new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - bufferSize: BufferSize, - makeChannels: OnMissingChannel.Create - )).GetAwaiter().GetResult(); - - //we want to access via a consumer, to receive multiple messages - we don't want to expose on channel - //just for the tests, so create a new consumer from the properties - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName(), BufferSize); - _messageProducer = new SqsMessageProducer(awsConnection, - new SnsPublication { MakeChannels = OnMissingChannel.Create }); - } - - [Fact] - public async Task When_a_message_consumer_reads_multiple_messages_async() - { - var routingKey = new RoutingKey(_topicName); - - var messageOne = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content one") - ); - - var messageTwo = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content two") - ); - - var messageThree = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content three") - ); - - var messageFour = new Message( - new MessageHeader(Guid.NewGuid().ToString(), routingKey, MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), contentType: ContentType), - new MessageBody("test content four") - ); - - //send MESSAGE_COUNT messages - await _messageProducer.SendAsync(messageOne); - await _messageProducer.SendAsync(messageTwo); - await _messageProducer.SendAsync(messageThree); - await _messageProducer.SendAsync(messageFour); - - int iteration = 0; - var messagesReceived = new List(); - var messagesReceivedCount = messagesReceived.Count; - do - { - iteration++; - var outstandingMessageCount = MessageCount - messagesReceivedCount; - - //retrieve messages - var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); - - messages.Length.Should().BeLessOrEqualTo(outstandingMessageCount); - - //should not receive more than buffer in one hit - messages.Length.Should().BeLessOrEqualTo(BufferSize); - - var moreMessages = messages.Where(m => m.Header.MessageType == MessageType.MT_COMMAND); - foreach (var message in moreMessages) - { - messagesReceived.Add(message); - await _consumer.AcknowledgeAsync(message); - } - - messagesReceivedCount = messagesReceived.Count; - - await Task.Delay(1000); - - } while ((iteration <= 5) && (messagesReceivedCount < MessageCount)); - - messagesReceivedCount.Should().Be(4); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await _messageProducer.DisposeAsync(); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().GetAwaiter().GetResult(); - _channelFactory.DeleteQueueAsync().GetAwaiter().GetResult(); - _messageProducer.DisposeAsync().GetAwaiter().GetResult(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs deleted file mode 100644 index 2ab69a3fd0..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class CustomisingAwsClientConfigTests : IDisposable, IAsyncDisposable - { - private readonly Message _message; - private readonly IAmAChannelSync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - - private readonly InterceptingDelegatingHandler _publishHttpHandler = new(); - private readonly InterceptingDelegatingHandler _subscribeHttpHandler = new(); - - public CustomisingAwsClientConfigTests() - { - MyCommand myCommand = new() {Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - messagePumpType: MessagePumpType.Reactor, - routingKey: routingKey - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var subscribeAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => - { - config.HttpClientFactory = new InterceptingHttpClientFactory(_subscribeHttpHandler); - }); - - _channelFactory = new ChannelFactory(subscribeAwsConnection); - _channel = _channelFactory.CreateSyncChannel(subscription); - - var publishAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => - { - config.HttpClientFactory = new InterceptingHttpClientFactory(_publishHttpHandler); - }); - - _messageProducer = new SqsMessageProducer(publishAwsConnection, new SnsPublication{Topic = new RoutingKey(topicName), MakeChannels = OnMissingChannel.Create}); - } - - [Fact] - public async Task When_customising_aws_client_config() - { - //arrange - _messageProducer.Send(_message); - - await Task.Delay(1000); - - var message =_channel.Receive(TimeSpan.FromMilliseconds(5000)); - - //clear the queue - _channel.Acknowledge(message); - - //publish_and_subscribe_should_use_custom_http_client_factory - _publishHttpHandler.RequestCount.Should().BeGreaterThan(0); - _subscribeHttpHandler.RequestCount.Should().BeGreaterThan(0); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs deleted file mode 100644 index 5f7be4ca28..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_customising_aws_client_config_async.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class CustomisingAwsClientConfigTestsAsync : IDisposable, IAsyncDisposable - { - private readonly Message _message; - private readonly IAmAChannelAsync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - - private readonly InterceptingDelegatingHandler _publishHttpHandler = new(); - private readonly InterceptingDelegatingHandler _subscribeHttpHandler = new(); - - public CustomisingAwsClientConfigTestsAsync() - { - MyCommand myCommand = new() {Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - messagePumpType: MessagePumpType.Proactor, - routingKey: routingKey - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var subscribeAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => - { - config.HttpClientFactory = new InterceptingHttpClientFactory(_subscribeHttpHandler); - }); - - _channelFactory = new ChannelFactory(subscribeAwsConnection); - _channel = _channelFactory.CreateAsyncChannel(subscription); - - var publishAwsConnection = new AWSMessagingGatewayConnection(credentials, region, config => - { - config.HttpClientFactory = new InterceptingHttpClientFactory(_publishHttpHandler); - }); - - _messageProducer = new SqsMessageProducer(publishAwsConnection, new SnsPublication{Topic = new RoutingKey(topicName), MakeChannels = OnMissingChannel.Create}); - } - - [Fact] - public async Task When_customising_aws_client_config() - { - //arrange - await _messageProducer.SendAsync(_message); - - await Task.Delay(1000); - - var message =await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - //clear the queue - await _channel.AcknowledgeAsync(message); - - //publish_and_subscribe_should_use_custom_http_client_factory - _publishHttpHandler.RequestCount.Should().BeGreaterThan(0); - _subscribeHttpHandler.RequestCount.Should().BeGreaterThan(0); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs deleted file mode 100644 index e0a5f37df0..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSAssumeInfrastructureTests : IDisposable, IAsyncDisposable - { private readonly Message _message; - private readonly SqsMessageConsumer _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSAssumeInfrastructureTests() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - //This doesn't look that different from our create tests - this is because we create using the channel factory in - //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateSyncChannel(subscription); - - //Now change the subscription to validate, just check what we made - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Assume - ); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Assume}); - - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); - } - - [Fact] - public void When_infastructure_exists_can_assume() - { - //arrange - _messageProducer.Send(_message); - - var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); - - //Assert - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - //clear the queue - _consumer.Acknowledge(message); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs deleted file mode 100644 index e1d5b3d92c..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_assume_async.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSAssumeInfrastructureTestsAsync : IDisposable, IAsyncDisposable - { private readonly Message _message; - private readonly SqsMessageConsumer _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSAssumeInfrastructureTestsAsync() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - //This doesn't look that different from our create tests - this is because we create using the channel factory in - //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateAsyncChannel(subscription); - - //Now change the subscription to validate, just check what we made - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Assume - ); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Assume}); - - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); - } - - [Fact] - public async Task When_infastructure_exists_can_assume() - { - //arrange - await _messageProducer.SendAsync(_message); - - var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - //Assert - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - //clear the queue - await _consumer.AcknowledgeAsync(message); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs deleted file mode 100644 index 1cb5504015..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureTests : IDisposable, IAsyncDisposable - { private readonly Message _message; - private readonly IAmAMessageConsumerSync _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSValidateInfrastructureTests() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - //This doesn't look that different from our create tests - this is because we create using the channel factory in - //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateSyncChannel(subscription); - - //Now change the subscription to validate, just check what we made - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKey, - findTopicBy: TopicFindBy.Name, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Validate - ); - - _messageProducer = new SqsMessageProducer( - awsConnection, - new SnsPublication - { - FindTopicBy = TopicFindBy.Name, - MakeChannels = OnMissingChannel.Validate, - Topic = new RoutingKey(topicName) - } - ); - - _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); - } - - [Fact] - public async Task When_infrastructure_exists_can_verify() - { - //arrange - _messageProducer.Send(_message); - - await Task.Delay(1000); - - var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); - - //Assert - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - //clear the queue - _consumer.Acknowledge(message); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _consumer.Dispose(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); - await _messageProducer.DisposeAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs deleted file mode 100644 index 0235994a8e..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_arn.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SimpleNotificationService; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByArnTests : IDisposable, IAsyncDisposable - { private readonly Message _message; - private readonly IAmAMessageConsumerSync _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSValidateInfrastructureByArnTests() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - //This doesn't look that different from our create tests - this is because we create using the channel factory in - //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateSyncChannel(subscription); - - var topicArn = FindTopicArn(credentials, region, routingKey.Value); - var routingKeyArn = new RoutingKey(topicArn); - - //Now change the subscription to validate, just check what we made - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKeyArn, - findTopicBy: TopicFindBy.Arn, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Validate - ); - - _messageProducer = new SqsMessageProducer( - awsConnection, - new SnsPublication - { - Topic = routingKey, - TopicArn = topicArn, - FindTopicBy = TopicFindBy.Arn, - MakeChannels = OnMissingChannel.Validate - }); - - _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); - } - - [Fact] - public async Task When_infrastructure_exists_can_verify() - { - //arrange - _messageProducer.Send(_message); - - await Task.Delay(1000); - - var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); - - //Assert - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - //clear the queue - _consumer.Acknowledge(message); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _consumer.Dispose(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); - await _messageProducer.DisposeAsync(); - } - - private string FindTopicArn(AWSCredentials credentials, RegionEndpoint region, string topicName) - { - var snsClient = new AmazonSimpleNotificationServiceClient(credentials, region); - var topicResponse = snsClient.FindTopicAsync(topicName).GetAwaiter().GetResult(); - return topicResponse.TopicArn; - } - - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs deleted file mode 100644 index 3f3a86ed27..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infastructure_exists_can_verify_by_convention.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByConventionTests : IDisposable, IAsyncDisposable - { private readonly Message _message; - private readonly IAmAMessageConsumerSync _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSValidateInfrastructureByConventionTests() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - //This doesn't look that different from our create tests - this is because we create using the channel factory in - //our AWS transport, not the consumer (as it's a more likely to use infrastructure declared elsewhere) - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateSyncChannel(subscription); - - //Now change the subscription to validate, just check what we made - will make the SNS Arn to prevent ListTopics call - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKey, - findTopicBy: TopicFindBy.Convention, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Validate - ); - - _messageProducer = new SqsMessageProducer( - awsConnection, - new SnsPublication{ - FindTopicBy = TopicFindBy.Convention, - MakeChannels = OnMissingChannel.Validate - } - ); - - _consumer = new SqsMessageConsumerFactory(awsConnection).Create(subscription); - } - - [Fact] - public async Task When_infrastructure_exists_can_verify() - { - //arrange - _messageProducer.Send(_message); - - await Task.Delay(1000); - - var messages = _consumer.Receive(TimeSpan.FromMilliseconds(5000)); - - //Assert - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - //clear the queue - _consumer.Acknowledge(message); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _consumer.Dispose(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await ((IAmAMessageConsumerAsync)_consumer).DisposeAsync(); - await _messageProducer.DisposeAsync(); - } - - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs deleted file mode 100644 index fcec7e29a2..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_infrastructure_exists_can_verify_by_arn_async.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SimpleNotificationService; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class AWSValidateInfrastructureByArnTestsAsync : IAsyncDisposable, IDisposable - { - private readonly Message _message; - private readonly IAmAMessageConsumerAsync _consumer; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public AWSValidateInfrastructureByArnTestsAsync() - { - _myCommand = new MyCommand { Value = "Test" }; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey($"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateAsyncChannel(subscription); - - var topicArn = FindTopicArn(credentials, region, routingKey.Value).Result; - var routingKeyArn = new RoutingKey(topicArn); - - subscription = new( - name: new SubscriptionName(channelName), - channelName: channel.Name, - routingKey: routingKeyArn, - findTopicBy: TopicFindBy.Arn, - makeChannels: OnMissingChannel.Validate - ); - - _messageProducer = new SqsMessageProducer( - awsConnection, - new SnsPublication - { - Topic = routingKey, - TopicArn = topicArn, - FindTopicBy = TopicFindBy.Arn, - MakeChannels = OnMissingChannel.Validate - }); - - _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); - } - - [Fact] - public async Task When_infrastructure_exists_can_verify_async() - { - await _messageProducer.SendAsync(_message); - - await Task.Delay(1000); - - var messages = await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - var message = messages.First(); - message.Id.Should().Be(_myCommand.Id); - - await _consumer.AcknowledgeAsync(message); - } - - private async Task FindTopicArn(AWSCredentials credentials, RegionEndpoint region, string topicName) - { - var snsClient = new AmazonSimpleNotificationServiceClient(credentials, region); - var topicResponse = await snsClient.FindTopicAsync(topicName); - return topicResponse.TopicArn; - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - ((IAmAMessageConsumerSync)_consumer).Dispose(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await _consumer.DisposeAsync(); - await _messageProducer.DisposeAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs deleted file mode 100644 index b23f62cc1b..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class SqsMessageProducerSendTests : IDisposable, IAsyncDisposable - { - private readonly Message _message; - private readonly IAmAChannelSync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - private readonly string _correlationId; - private readonly string _replyTo; - private readonly string _contentType; - private readonly string _topicName; - - public SqsMessageProducerSendTests() - { - _myCommand = new MyCommand{Value = "Test"}; - _correlationId = Guid.NewGuid().ToString(); - _replyTo = "http:\\queueUrl"; - _contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(_topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - rawMessageDelivery: false - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, - replyTo: new RoutingKey(_replyTo), contentType: _contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateSyncChannel(subscription); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create}); - } - - - - [Fact] - public async Task When_posting_a_message_via_the_producer() - { - //arrange - _message.Header.Subject = "test subject"; - _messageProducer.Send(_message); - - await Task.Delay(1000); - - var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - - //clear the queue - _channel.Acknowledge(message); - - //should_send_the_message_to_aws_sqs - message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - - message.Id.Should().Be(_myCommand.Id); - message.Redelivered.Should().BeFalse(); - message.Header.MessageId.Should().Be(_myCommand.Id); - message.Header.Topic.Value.Should().Contain(_topicName); - message.Header.CorrelationId.Should().Be(_correlationId); - message.Header.ReplyTo.Should().Be(_replyTo); - message.Header.ContentType.Should().Be(_contentType); - message.Header.HandledCount.Should().Be(0); - message.Header.Subject.Should().Be(_message.Header.Subject); - //allow for clock drift in the following test, more important to have a contemporary timestamp than anything - message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); - message.Header.Delayed.Should().Be(TimeSpan.Zero); - //{"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} - message.Body.Value.Should().Be(_message.Body.Value); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await _messageProducer.DisposeAsync(); - } - - private static DateTime RoundToSeconds(DateTime dateTime) - { - return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); - } - - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs deleted file mode 100644 index c25b8f5b88..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_posting_a_message_via_the_messaging_gateway_async.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class SqsMessageProducerSendAsyncTests : IAsyncDisposable, IDisposable - { - private readonly Message _message; - private readonly IAmAChannelAsync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - private readonly string _correlationId; - private readonly string _replyTo; - private readonly string _contentType; - private readonly string _topicName; - - public SqsMessageProducerSendAsyncTests() - { - _myCommand = new MyCommand { Value = "Test" }; - _correlationId = Guid.NewGuid().ToString(); - _replyTo = "http:\\queueUrl"; - _contentType = "text\\plain"; - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(_topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - rawMessageDelivery: false - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: _correlationId, - replyTo: new RoutingKey(_replyTo), contentType: _contentType), - new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateAsyncChannel(subscription); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication { Topic = new RoutingKey(_topicName), MakeChannels = OnMissingChannel.Create }); - } - - [Fact] - public async Task When_posting_a_message_via_the_producer_async() - { - // arrange - _message.Header.Subject = "test subject"; - await _messageProducer.SendAsync(_message); - - await Task.Delay(1000); - - var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - // clear the queue - await _channel.AcknowledgeAsync(message); - - // should_send_the_message_to_aws_sqs - message.Header.MessageType.Should().Be(MessageType.MT_COMMAND); - - message.Id.Should().Be(_myCommand.Id); - message.Redelivered.Should().BeFalse(); - message.Header.MessageId.Should().Be(_myCommand.Id); - message.Header.Topic.Value.Should().Contain(_topicName); - message.Header.CorrelationId.Should().Be(_correlationId); - message.Header.ReplyTo.Should().Be(_replyTo); - message.Header.ContentType.Should().Be(_contentType); - message.Header.HandledCount.Should().Be(0); - message.Header.Subject.Should().Be(_message.Header.Subject); - // allow for clock drift in the following test, more important to have a contemporary timestamp than anything - message.Header.TimeStamp.Should().BeAfter(RoundToSeconds(DateTime.UtcNow.AddMinutes(-1))); - message.Header.Delayed.Should().Be(TimeSpan.Zero); - // {"Id":"cd581ced-c066-4322-aeaf-d40944de8edd","Value":"Test","WasCancelled":false,"TaskCompleted":false} - message.Body.Value.Should().Be(_message.Body.Value); - } - - public void Dispose() - { - //Clean up resources that we have created - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - _messageProducer.Dispose(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - await _messageProducer.DisposeAsync(); - } - - private static DateTime RoundToSeconds(DateTime dateTime) - { - return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs deleted file mode 100644 index 54c18811b2..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS.Model; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSAssumeQueuesTests : IDisposable, IAsyncDisposable - { - private readonly ChannelFactory _channelFactory; - private readonly SqsMessageConsumer _consumer; - - public AWSAssumeQueuesTests() - { - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - var subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Assume - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //create the topic, we want the queue to be the issue - //We need to create the topic at least, to check the queues - var producer = new SqsMessageProducer(awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - - producer.ConfirmTopicExistsAsync(topicName).Wait(); - - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateSyncChannel(subscription); - - //We need to create the topic at least, to check the queues - _consumer = new SqsMessageConsumer(awsConnection, channel.Name.ToValidSQSQueueName()); - } - - [Fact] - public void When_queues_missing_assume_throws() - { - //we will try to get the queue url, and fail because it does not exist - Assert.Throws(() => _consumer.Receive(TimeSpan.FromMilliseconds(1000))); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - } - - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs deleted file mode 100644 index 178316874f..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_assume_throws_async.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS.Model; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSAssumeQueuesTestsAsync : IAsyncDisposable, IDisposable - { - private readonly ChannelFactory _channelFactory; - private readonly IAmAMessageConsumerAsync _consumer; - - public AWSAssumeQueuesTestsAsync() - { - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - var subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - makeChannels: OnMissingChannel.Assume, - messagePumpType: MessagePumpType.Proactor - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //create the topic, we want the queue to be the issue - //We need to create the topic at least, to check the queues - var producer = new SqsMessageProducer(awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - - producer.ConfirmTopicExistsAsync(topicName).Wait(); - - _channelFactory = new ChannelFactory(awsConnection); - var channel = _channelFactory.CreateAsyncChannel(subscription); - - //We need to create the topic at least, to check the queues - _consumer = new SqsMessageConsumerFactory(awsConnection).CreateAsync(subscription); - } - - [Fact] - public async Task When_queues_missing_assume_throws_async() - { - //we will try to get the queue url, and fail because it does not exist - await Assert.ThrowsAsync(async () => await _consumer.ReceiveAsync(TimeSpan.FromMilliseconds(1000))); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs deleted file mode 100644 index 8179fd938f..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS.Model; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSValidateQueuesTests : IDisposable, IAsyncDisposable - { - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly SqsSubscription _subscription; - private ChannelFactory _channelFactory; - - public AWSValidateQueuesTests() - { - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - _subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Reactor, - makeChannels: OnMissingChannel.Validate - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to create the topic at least, to check the queues - var producer = new SqsMessageProducer(_awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - producer.ConfirmTopicExistsAsync(topicName).Wait(); - - } - - [Fact] - public void When_queues_missing_verify_throws() - { - //We have no queues so we should throw - //We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(_awsConnection); - Assert.Throws(() => _channelFactory.CreateSyncChannel(_subscription)); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs deleted file mode 100644 index 4fb96ac19c..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_queues_missing_verify_throws_async.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS.Model; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSValidateQueuesTestsAsync : IAsyncDisposable - { - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly SqsSubscription _subscription; - private ChannelFactory _channelFactory; - - public AWSValidateQueuesTestsAsync() - { - var channelName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - _subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - makeChannels: OnMissingChannel.Validate - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - // We need to create the topic at least, to check the queues - var producer = new SqsMessageProducer(_awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - producer.ConfirmTopicExistsAsync(topicName).Wait(); - } - - [Fact] - public async Task When_queues_missing_verify_throws_async() - { - // We have no queues so we should throw - // We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(_awsConnection); - await Assert.ThrowsAsync(async () => await _channelFactory.CreateAsyncChannelAsync(_subscription)); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs deleted file mode 100644 index 0d60c63afb..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsRawMessageDeliveryTests : IDisposable, IAsyncDisposable - { - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly IAmAChannelSync _channel; - private readonly RoutingKey _routingKey; - - public SqsRawMessageDeliveryTests() - { - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); - - var bufferSize = 10; - - //Set rawMessageDelivery to false - _channel = _channelFactory.CreateSyncChannel(new SqsSubscription( - name: new SubscriptionName(channelName), - channelName:new ChannelName(channelName), - routingKey:_routingKey, - bufferSize: bufferSize, - makeChannels: OnMissingChannel.Create, - messagePumpType: MessagePumpType.Reactor, - rawMessageDelivery: false)); - - _messageProducer = new SqsMessageProducer(awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - } - - [Fact] - public void When_raw_message_delivery_disabled() - { - //arrange - var messageHeader = new MessageHeader( - Guid.NewGuid().ToString(), - _routingKey, - MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), - replyTo: RoutingKey.Empty, - contentType: "text\\plain"); - - var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); - messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); - - var messageToSent = new Message(messageHeader, new MessageBody("test content one")); - - //act - _messageProducer.Send(messageToSent); - - var messageReceived = _channel.Receive(TimeSpan.FromMilliseconds(10000)); - - _channel.Acknowledge(messageReceived); - - //assert - messageReceived.Id.Should().Be(messageToSent.Id); - messageReceived.Header.Topic.Should().Be(messageToSent.Header.Topic); - messageReceived.Header.MessageType.Should().Be(messageToSent.Header.MessageType); - messageReceived.Header.CorrelationId.Should().Be(messageToSent.Header.CorrelationId); - messageReceived.Header.ReplyTo.Should().Be(messageToSent.Header.ReplyTo); - messageReceived.Header.ContentType.Should().Be(messageToSent.Header.ContentType); - messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); - messageReceived.Body.Value.Should().Be(messageToSent.Body.Value); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs deleted file mode 100644 index ad435b5a46..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_raw_message_delivery_disabled_async.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsRawMessageDeliveryTestsAsync : IAsyncDisposable, IDisposable - { - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly IAmAChannelAsync _channel; - private readonly RoutingKey _routingKey; - - public SqsRawMessageDeliveryTestsAsync() - { - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - var channelName = $"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _routingKey = new RoutingKey($"Raw-Msg-Delivery-Tests-{Guid.NewGuid().ToString()}".Truncate(45)); - - var bufferSize = 10; - - // Set rawMessageDelivery to false - _channel = _channelFactory.CreateAsyncChannel(new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: _routingKey, - bufferSize: bufferSize, - makeChannels: OnMissingChannel.Create, - rawMessageDelivery: false)); - - _messageProducer = new SqsMessageProducer(awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Create - }); - } - - [Fact] - public async Task When_raw_message_delivery_disabled_async() - { - // Arrange - var messageHeader = new MessageHeader( - Guid.NewGuid().ToString(), - _routingKey, - MessageType.MT_COMMAND, - correlationId: Guid.NewGuid().ToString(), - replyTo: RoutingKey.Empty, - contentType: "text\\plain"); - - var customHeaderItem = new KeyValuePair("custom-header-item", "custom-header-item-value"); - messageHeader.Bag.Add(customHeaderItem.Key, customHeaderItem.Value); - - var messageToSend = new Message(messageHeader, new MessageBody("test content one")); - - // Act - await _messageProducer.SendAsync(messageToSend); - - var messageReceived = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(10000)); - - await _channel.AcknowledgeAsync(messageReceived); - - // Assert - messageReceived.Id.Should().Be(messageToSend.Id); - messageReceived.Header.Topic.Should().Be(messageToSend.Header.Topic); - messageReceived.Header.MessageType.Should().Be(messageToSend.Header.MessageType); - messageReceived.Header.CorrelationId.Should().Be(messageToSend.Header.CorrelationId); - messageReceived.Header.ReplyTo.Should().Be(messageToSend.Header.ReplyTo); - messageReceived.Header.ContentType.Should().Be(messageToSend.Header.ContentType); - messageReceived.Header.Bag.Should().ContainKey(customHeaderItem.Key).And.ContainValue(customHeaderItem.Value); - messageReceived.Body.Value.Should().Be(messageToSend.Body.Value); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs deleted file mode 100644 index 644d940496..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsMessageConsumerRequeueTests : IDisposable - { - private readonly Message _message; - private readonly IAmAChannelSync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public SqsMessageConsumerRequeueTests() - { - _myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - messagePumpType: MessagePumpType.Reactor, - routingKey: routingKey - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) _myCommand, JsonSerialisationOptions.Options)) - ); - - //Must have credentials stored in the SDK Credentials store or shared credentials file - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateSyncChannel(subscription); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); - } - - [Fact] - public void When_rejecting_a_message_through_gateway_with_requeue() - { - _messageProducer.Send(_message); - - var message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - - _channel.Reject(message); - - //Let the timeout change - Task.Delay(TimeSpan.FromMilliseconds(3000)); - - //should requeue_the_message - message = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - - //clear the queue - _channel.Acknowledge(message); - - message.Id.Should().Be(_myCommand.Id); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs deleted file mode 100644 index 3af75acf8e..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_rejecting_a_message_through_gateway_with_requeue_async.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsMessageConsumerRequeueTestsAsync : IDisposable, IAsyncDisposable - { - private readonly Message _message; - private readonly IAmAChannelAsync _channel; - private readonly SqsMessageProducer _messageProducer; - private readonly ChannelFactory _channelFactory; - private readonly MyCommand _myCommand; - - public SqsMessageConsumerRequeueTestsAsync() - { - _myCommand = new MyCommand { Value = "Test" }; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Consumer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(_myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)_myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateAsyncChannel(subscription); - - _messageProducer = new SqsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); - } - - [Fact] - public async Task When_rejecting_a_message_through_gateway_with_requeue_async() - { - await _messageProducer.SendAsync(_message); - - var message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - await _channel.RejectAsync(message); - - // Let the timeout change - await Task.Delay(TimeSpan.FromMilliseconds(3000)); - - // should requeue_the_message - message = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - // clear the queue - await _channel.AcknowledgeAsync(message); - - message.Id.Should().Be(_myCommand.Id); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs deleted file mode 100644 index 4f28d6c3db..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.Runtime.CredentialManagement; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class SqsMessageProducerRequeueTests : IDisposable, IAsyncDisposable - { - private readonly IAmAMessageProducerSync _sender; - private Message _requeuedMessage; - private Message _receivedMessage; - private readonly IAmAChannelSync _channel; - private readonly ChannelFactory _channelFactory; - private readonly Message _message; - - public SqsMessageProducerRequeueTests() - { - MyCommand myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - var subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) - ); - - //Must have credentials stored in the SDK Credentials store or shared credentials file - new CredentialProfileStoreChain(); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _sender = new SqsMessageProducer(awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); - - //We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateSyncChannel(subscription); - } - - [Fact] - public void When_requeueing_a_message() - { - _sender.Send(_message); - _receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - _channel.Requeue(_receivedMessage); - - _requeuedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - - //clear the queue - _channel.Acknowledge(_requeuedMessage ); - - _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs deleted file mode 100644 index faa37a7128..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_a_message_async.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.Runtime.CredentialManagement; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class SqsMessageProducerRequeueTestsAsync : IDisposable, IAsyncDisposable - { - private readonly IAmAMessageProducerAsync _sender; - private Message _requeuedMessage; - private Message _receivedMessage; - private readonly IAmAChannelAsync _channel; - private readonly ChannelFactory _channelFactory; - private readonly Message _message; - - public SqsMessageProducerRequeueTestsAsync() - { - MyCommand myCommand = new MyCommand { Value = "Test" }; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-Requeue-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - var subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - makeChannels: OnMissingChannel.Create - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) - ); - - new CredentialProfileStoreChain(); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - var awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _sender = new SqsMessageProducer(awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); - - _channelFactory = new ChannelFactory(awsConnection); - _channel = _channelFactory.CreateAsyncChannel(subscription); - } - - [Fact] - public async Task When_requeueing_a_message_async() - { - await _sender.SendAsync(_message); - _receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - await _channel.RequeueAsync(_receivedMessage); - - _requeuedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - - await _channel.AcknowledgeAsync(_requeuedMessage); - - _requeuedMessage.Body.Value.Should().Be(_receivedMessage.Body.Value); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs deleted file mode 100644 index 200cfc9810..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS; -using Amazon.SQS.Model; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsMessageProducerDlqTests : IDisposable, IAsyncDisposable - { - private readonly SqsMessageProducer _sender; - private readonly IAmAChannelSync _channel; - private readonly ChannelFactory _channelFactory; - private readonly Message _message; - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly string _dlqChannelName; - - public SqsMessageProducerDlqTests () - { - MyCommand myCommand = new MyCommand{Value = "Test"}; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _dlqChannelName =$"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object) myCommand, JsonSerialisationOptions.Options)) - ); - - //Must have credentials stored in the SDK Credentials store or shared credentials file - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _sender = new SqsMessageProducer(_awsConnection, new SnsPublication{MakeChannels = OnMissingChannel.Create}); - - _sender.ConfirmTopicExistsAsync(topicName).Wait(); - - //We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(_awsConnection); - _channel = _channelFactory.CreateSyncChannel(subscription); - } - - [Fact] - public void When_requeueing_redrives_to_the_queue() - { - _sender.Send(_message); - var receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - _channel.Requeue(receivedMessage ); - - receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - _channel.Requeue(receivedMessage ); - - //should force us into the dlq - receivedMessage = _channel.Receive(TimeSpan.FromMilliseconds(5000)); - _channel.Requeue(receivedMessage) ; - - Task.Delay(5000); - - //inspect the dlq - GetDLQCount(_dlqChannelName).Should().Be(1); - } - - public int GetDLQCount(string queueName) - { - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); - var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest - { - QueueUrl = queueUrlResponse.QueueUrl, - WaitTimeSeconds = 5, - MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } - }).GetAwaiter().GetResult(); - - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); - } - - return response.Messages.Count; - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs deleted file mode 100644 index 65d1fc3706..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_requeueing_redrives_to_the_dlq_async.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS; -using Amazon.SQS.Model; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SqsMessageProducerDlqTestsAsync : IDisposable, IAsyncDisposable - { - private readonly SqsMessageProducer _sender; - private readonly IAmAChannelAsync _channel; - private readonly ChannelFactory _channelFactory; - private readonly Message _message; - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly string _dlqChannelName; - - public SqsMessageProducerDlqTestsAsync() - { - MyCommand myCommand = new MyCommand { Value = "Test" }; - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _dlqChannelName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Producer-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - SqsSubscription subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - messagePumpType: MessagePumpType.Proactor, - redrivePolicy: new RedrivePolicy(_dlqChannelName, 2) - ); - - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _sender = new SqsMessageProducer(_awsConnection, new SnsPublication { MakeChannels = OnMissingChannel.Create }); - - _sender.ConfirmTopicExistsAsync(topicName).Wait(); - - _channelFactory = new ChannelFactory(_awsConnection); - _channel = _channelFactory.CreateAsyncChannel(subscription); - } - - [Fact] - public async Task When_requeueing_redrives_to_the_queue_async() - { - await _sender.SendAsync(_message); - var receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - await _channel.RequeueAsync(receivedMessage); - - receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - await _channel.RequeueAsync(receivedMessage); - - receivedMessage = await _channel.ReceiveAsync(TimeSpan.FromMilliseconds(5000)); - await _channel.RequeueAsync(receivedMessage); - - await Task.Delay(5000); - - int dlqCount = await GetDLQCountAsync(_dlqChannelName); - dlqCount.Should().Be(1); - } - - public async Task GetDLQCountAsync(string queueName) - { - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); - var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest - { - QueueUrl = queueUrlResponse.QueueUrl, - WaitTimeSeconds = 5, - MessageAttributeNames = new List { "All", "ApproximateReceiveCount" } - }); - - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); - } - - return response.Messages.Count; - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs deleted file mode 100644 index 6572e26a60..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS; -using Amazon.SQS.Model; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Paramore.Brighter.ServiceActivator; -using Polly.Registry; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SnsReDrivePolicySDlqTests - { - private readonly IAmAMessagePump _messagePump; - private readonly Message _message; - private readonly string _dlqChannelName; - private readonly IAmAChannelSync _channel; - private readonly SqsMessageProducer _sender; - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly SqsSubscription _subscription; - private readonly ChannelFactory _channelFactory; - - public SnsReDrivePolicySDlqTests() - { - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - //how are we consuming - _subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - //don't block the redrive policy from owning retry management - requeueCount: -1, - //delay before requeuing - requeueDelay: TimeSpan.FromMilliseconds(50), - messagePumpType: MessagePumpType.Reactor, - //we want our SNS subscription to manage requeue limits using the DLQ for 'too many requeues' - redrivePolicy: new RedrivePolicy - ( - deadLetterQueueName: new ChannelName(_dlqChannelName), - maxReceiveCount: 2 - )); - - //what do we send - var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) - ); - - //Must have credentials stored in the SDK Credentials store or shared credentials file - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //how do we send to the queue - _sender = new SqsMessageProducer( - _awsConnection, - new SnsPublication - { - Topic = routingKey, - RequestType = typeof(MyDeferredCommand), - MakeChannels = OnMissingChannel.Create - } - ); - - //We need to do this manually in a test - will create the channel from subscriber parameters - _channelFactory = new ChannelFactory(_awsConnection); - _channel = _channelFactory.CreateSyncChannel(_subscription); - - //how do we handle a command - IHandleRequests handler = new MyDeferredCommandHandler(); - - //hook up routing for the command processor - var subscriberRegistry = new SubscriberRegistry(); - subscriberRegistry.Register(); - - //once we read, how do we dispatch to a handler. N.B. we don't use this for reading here - IAmACommandProcessor commandProcessor = new CommandProcessor( - subscriberRegistry: subscriberRegistry, - handlerFactory: new QuickHandlerFactory(() => handler), - requestContextFactory: new InMemoryRequestContextFactory(), - policyRegistry: new PolicyRegistry() - ); - - var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), - null - ); - messageMapperRegistry.Register(); - - //pump messages from a channel to a handler - in essence we are building our own dispatcher in this test - _messagePump = new Reactor(commandProcessor, messageMapperRegistry, - null, new InMemoryRequestContextFactory(), _channel) - { - Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 - }; - } - - public int GetDLQCount(string queueName) - { - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - var queueUrlResponse = sqsClient.GetQueueUrlAsync(queueName).GetAwaiter().GetResult(); - var response = sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest - { - QueueUrl = queueUrlResponse.QueueUrl, - WaitTimeSeconds = 5, - MessageSystemAttributeNames = ["ApproximateReceiveCount"], - MessageAttributeNames = new List { "All" } - }).GetAwaiter().GetResult(); - - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); - } - - return response.Messages.Count; - } - - - [Fact] - public async Task When_throwing_defer_action_respect_redrive() - { - //put something on an SNS topic, which will be delivered to our SQS queue - _sender.Send(_message); - - //start a message pump, let it process messages - var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); - await Task.Delay(5000); - - //send a quit message to the pump to terminate it - var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); - _channel.Enqueue(quitMessage); - - //wait for the pump to stop once it gets a quit message - await Task.WhenAll(task); - - await Task.Delay(5000); - - //inspect the dlq - GetDLQCount(_dlqChannelName).Should().Be(1); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs deleted file mode 100644 index e1505e3d47..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_throwing_defer_action_respect_redrive_async.cs +++ /dev/null @@ -1,152 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Amazon.SQS; -using Amazon.SQS.Model; -using FluentAssertions; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.AWS.Tests.TestDoubles; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Paramore.Brighter.ServiceActivator; -using Polly.Registry; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - [Trait("Fragile", "CI")] - public class SnsReDrivePolicySDlqTestsAsync : IDisposable, IAsyncDisposable - { - private readonly IAmAMessagePump _messagePump; - private readonly Message _message; - private readonly string _dlqChannelName; - private readonly IAmAChannelAsync _channel; - private readonly SqsMessageProducer _sender; - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly SqsSubscription _subscription; - private readonly ChannelFactory _channelFactory; - - public SnsReDrivePolicySDlqTestsAsync() - { - string correlationId = Guid.NewGuid().ToString(); - string replyTo = "http:\\queueUrl"; - string contentType = "text\\plain"; - var channelName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _dlqChannelName = $"Redrive-DLQ-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - string topicName = $"Redrive-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - var routingKey = new RoutingKey(topicName); - - _subscription = new SqsSubscription( - name: new SubscriptionName(channelName), - channelName: new ChannelName(channelName), - routingKey: routingKey, - requeueCount: -1, - requeueDelay: TimeSpan.FromMilliseconds(50), - messagePumpType: MessagePumpType.Proactor, - redrivePolicy: new RedrivePolicy(new ChannelName(_dlqChannelName), 2) - ); - - var myCommand = new MyDeferredCommand { Value = "Hello Redrive" }; - _message = new Message( - new MessageHeader(myCommand.Id, routingKey, MessageType.MT_COMMAND, correlationId: correlationId, - replyTo: new RoutingKey(replyTo), contentType: contentType), - new MessageBody(JsonSerializer.Serialize((object)myCommand, JsonSerialisationOptions.Options)) - ); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - _sender = new SqsMessageProducer( - _awsConnection, - new SnsPublication - { - Topic = routingKey, - RequestType = typeof(MyDeferredCommand), - MakeChannels = OnMissingChannel.Create - } - ); - - _channelFactory = new ChannelFactory(_awsConnection); - _channel = _channelFactory.CreateAsyncChannel(_subscription); - - IHandleRequestsAsync handler = new MyDeferredCommandHandlerAsync(); - - var subscriberRegistry = new SubscriberRegistry(); - subscriberRegistry.RegisterAsync(); - - IAmACommandProcessor commandProcessor = new CommandProcessor( - subscriberRegistry: subscriberRegistry, - handlerFactory: new QuickHandlerFactoryAsync(() => handler), - requestContextFactory: new InMemoryRequestContextFactory(), - policyRegistry: new PolicyRegistry() - ); - - var messageMapperRegistry = new MessageMapperRegistry( - new SimpleMessageMapperFactory(_ => new MyDeferredCommandMessageMapper()), - null - ); - messageMapperRegistry.Register(); - - _messagePump = new Proactor(commandProcessor, messageMapperRegistry, - new EmptyMessageTransformerFactoryAsync(), new InMemoryRequestContextFactory(), _channel) - { - Channel = _channel, TimeOut = TimeSpan.FromMilliseconds(5000), RequeueCount = 3 - }; - } - - public async Task GetDLQCountAsync(string queueName) - { - using var sqsClient = new AmazonSQSClient(_awsConnection.Credentials, _awsConnection.Region); - var queueUrlResponse = await sqsClient.GetQueueUrlAsync(queueName); - var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest - { - QueueUrl = queueUrlResponse.QueueUrl, - WaitTimeSeconds = 5, - MessageSystemAttributeNames = new List { "ApproximateReceiveCount" }, - MessageAttributeNames = new List { "All" } - }); - - if (response.HttpStatusCode != HttpStatusCode.OK) - { - throw new AmazonSQSException($"Failed to GetMessagesAsync for queue {queueName}. Response: {response.HttpStatusCode}"); - } - - return response.Messages.Count; - } - - [Fact(Skip = "Failing async tests caused by task scheduler issues")] - public async Task When_throwing_defer_action_respect_redrive_async() - { - await _sender.SendAsync(_message); - - var task = Task.Factory.StartNew(() => _messagePump.Run(), TaskCreationOptions.LongRunning); - await Task.Delay(5000); - - var quitMessage = MessageFactory.CreateQuitMessage(_subscription.RoutingKey); - _channel.Enqueue(quitMessage); - - await Task.WhenAll(task); - - await Task.Delay(5000); - - int dlqCount = await GetDLQCountAsync(_dlqChannelName); - dlqCount.Should().Be(1); - } - - public void Dispose() - { - _channelFactory.DeleteTopicAsync().Wait(); - _channelFactory.DeleteQueueAsync().Wait(); - } - - public async ValueTask DisposeAsync() - { - await _channelFactory.DeleteTopicAsync(); - await _channelFactory.DeleteQueueAsync(); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws.cs deleted file mode 100644 index 8d663b20ed..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Amazon; -using Amazon.Runtime; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSValidateMissingTopicTests - { - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly RoutingKey _routingKey; - - public AWSValidateMissingTopicTests() - { - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _routingKey = new RoutingKey(topicName); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - //Because we don't use channel factory to create the infrastructure -it won't exist - } - - [Fact] - public void When_topic_missing_verify_throws() - { - //arrange - var producer = new SqsMessageProducer(_awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Validate - }); - - //act && assert - Assert.Throws(() => producer.Send(new Message( - new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), - new MessageBody("Test")))); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs b/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs deleted file mode 100644 index dc3102fc53..0000000000 --- a/tests/Paramore.Brighter.AWS.Tests/MessagingGateway/When_topic_missing_verify_throws_async.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Threading.Tasks; -using Amazon; -using Amazon.Runtime; -using Paramore.Brighter.AWS.Tests.Helpers; -using Paramore.Brighter.MessagingGateway.AWSSQS; -using Xunit; - -namespace Paramore.Brighter.AWS.Tests.MessagingGateway -{ - [Trait("Category", "AWS")] - public class AWSValidateMissingTopicTestsAsync - { - private readonly AWSMessagingGatewayConnection _awsConnection; - private readonly RoutingKey _routingKey; - - public AWSValidateMissingTopicTestsAsync() - { - string topicName = $"Producer-Send-Tests-{Guid.NewGuid().ToString()}".Truncate(45); - _routingKey = new RoutingKey(topicName); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _awsConnection = new AWSMessagingGatewayConnection(credentials, region); - - // Because we don't use channel factory to create the infrastructure - it won't exist - } - - [Fact] - public async Task When_topic_missing_verify_throws_async() - { - // arrange - var producer = new SqsMessageProducer(_awsConnection, - new SnsPublication - { - MakeChannels = OnMissingChannel.Validate - }); - - // act & assert - await Assert.ThrowsAsync(async () => - await producer.SendAsync(new Message( - new MessageHeader("", _routingKey, MessageType.MT_EVENT, type: "plain/text"), - new MessageBody("Test")))); - } - } -} diff --git a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj index a2039e9a67..6efd2fd56d 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj +++ b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj @@ -28,4 +28,8 @@ + + + + diff --git a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs index a6228a8a5f..12500f660e 100644 --- a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs +++ b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/MyDeferredCommandHandlerAsync.cs @@ -1,8 +1,8 @@ using System.Threading; using System.Threading.Tasks; -using Paramore.Brighter; using Paramore.Brighter.Actions; -using Paramore.Brighter.AWS.Tests.TestDoubles; + +namespace Paramore.Brighter.AWS.Tests.TestDoubles; internal class MyDeferredCommandHandlerAsync : RequestHandlerAsync { @@ -19,4 +19,4 @@ public override async Task HandleAsync(MyDeferredCommand comm return await base.HandleAsync(command, cancellationToken); } -} +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs index 35ccc61458..183b3f55a8 100644 --- a/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs +++ b/tests/Paramore.Brighter.AWS.Tests/TestDoubles/QuickHandlerFactoryAsync.cs @@ -1,5 +1,6 @@ using System; -using Paramore.Brighter; + +namespace Paramore.Brighter.AWS.Tests.TestDoubles; public class QuickHandlerFactoryAsync : IAmAHandlerFactoryAsync { @@ -19,4 +20,4 @@ public void Release(IHandleRequestsAsync handler, IAmALifetime lifetime) { // Implement any necessary cleanup logic here } -} +} \ No newline at end of file diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs index 55721627f6..dc1dd5e49f 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_creating_luggagestore_missing_parameters.cs @@ -25,10 +25,10 @@ public class S3LuggageUploadMissingParametersTests public S3LuggageUploadMissingParametersTests() { //arrange - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var factory = new AWSClientFactory(GatewayFactory.CreateFactory()); - _client = new AmazonS3Client(credentials, region); - _stsClient = new AmazonSecurityTokenServiceClient(credentials, region); + _client = factory.CreateS3Client(); + _stsClient = factory.CreateStsClient(); var services = new ServiceCollection(); services.AddHttpClient(); diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs index 2fe2e8c5cd..e8643ac374 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_unwrapping_a_large_message.cs @@ -19,7 +19,7 @@ namespace Paramore.Brighter.AWS.Tests.Transformers { - [Trait("Category", "AWS")] + [Trait("Category", "AWS")] [Trait("Fragile", "CI")] public class LargeMessagePaylodUnwrapTests : IDisposable { @@ -37,13 +37,13 @@ public LargeMessagePaylodUnwrapTests() new SimpleMessageMapperFactory(_ => new MyLargeCommandMessageMapper()), null ); - + mapperRegistry.Register(); - - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - _client = new AmazonS3Client(credentials, region); - AmazonSecurityTokenServiceClient stsClient = new(credentials, region); + var factory = new AWSClientFactory(GatewayFactory.CreateFactory()); + + _client = factory.CreateS3Client(); + var stsClient = factory.CreateStsClient(); var services = new ServiceCollection(); services.AddHttpClient(); @@ -51,7 +51,7 @@ public LargeMessagePaylodUnwrapTests() IHttpClientFactory httpClientFactory = provider.GetService(); _bucketName = $"brightertestbucket-{Guid.NewGuid()}"; - + _luggageStore = S3LuggageStore .CreateAsync( client: _client, @@ -61,7 +61,7 @@ public LargeMessagePaylodUnwrapTests() stsClient: stsClient, #pragma warning disable CS0618 // although obsolete, the region string on the replacement is wrong for our purpose bucketRegion: S3Region.EUW1, -#pragma warning restore CS0618 +#pragma warning restore CS0618 tags: new List { new Tag { Key = "BrighterTests", Value = "S3LuggageUploadTests" } }, acl: S3CannedACL.Private, abortFailedUploadsAfterDays: 1, @@ -69,23 +69,25 @@ public LargeMessagePaylodUnwrapTests() .GetAwaiter() .GetResult(); - var messageTransformerFactory = new SimpleMessageTransformerFactoryAsync(_ => new ClaimCheckTransformerAsync(_luggageStore)); + var messageTransformerFactory = + new SimpleMessageTransformerFactoryAsync(_ => new ClaimCheckTransformerAsync(_luggageStore)); _pipelineBuilder = new TransformPipelineBuilderAsync(mapperRegistry, messageTransformerFactory); } - + [Fact] public async Task When_unwrapping_a_large_message() { //arrange await Task.Delay(3000); //allow bucket definition to propagate - + //store our luggage and get the claim check var contents = DataGenerator.CreateString(6000); var myCommand = new MyLargeCommand(1) { Value = contents }; - var commandAsJson = JsonSerializer.Serialize(myCommand, new JsonSerializerOptions(JsonSerializerDefaults.General)); - - var stream = new MemoryStream(); + var commandAsJson = + JsonSerializer.Serialize(myCommand, new JsonSerializerOptions(JsonSerializerDefaults.General)); + + var stream = new MemoryStream(); var writer = new StreamWriter(stream); await writer.WriteAsync(commandAsJson); await writer.FlushAsync(); @@ -94,12 +96,13 @@ public async Task When_unwrapping_a_large_message() //pretend we ran through the claim check myCommand.Value = $"Claim Check {id}"; - + //set the headers, so that we have a claim check listed var message = new Message( - new MessageHeader(myCommand.Id, new RoutingKey("MyLargeCommand"), MessageType.MT_COMMAND, + new MessageHeader(myCommand.Id, new RoutingKey("MyLargeCommand"), MessageType.MT_COMMAND, timeStamp: DateTime.UtcNow), - new MessageBody(JsonSerializer.Serialize(myCommand, new JsonSerializerOptions(JsonSerializerDefaults.General))) + new MessageBody(JsonSerializer.Serialize(myCommand, + new JsonSerializerOptions(JsonSerializerDefaults.General))) ); message.Header.Bag[ClaimCheckTransformerAsync.CLAIM_CHECK] = id; @@ -107,7 +110,7 @@ public async Task When_unwrapping_a_large_message() //act var transformPipeline = _pipelineBuilder.BuildUnwrapPipeline(); var transformedMessage = await transformPipeline.UnwrapAsync(message, new RequestContext()); - + //assert //contents should be from storage transformedMessage.Value.Should().Be(contents); diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs index dab8b8c7d1..b33494b163 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_uploading_luggage_to_S3.cs @@ -31,10 +31,9 @@ public class S3LuggageUploadTests : IDisposable public S3LuggageUploadTests() { //arrange - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - - _client = new AmazonS3Client(credentials, region); - _stsClient = new AmazonSecurityTokenServiceClient(credentials, region); + var factory = new AWSClientFactory(GatewayFactory.CreateFactory()); + _client = factory.CreateS3Client(); + _stsClient = factory.CreateStsClient(); var services = new ServiceCollection(); services.AddHttpClient(); diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs index f1cec1cdd7..5efa46e871 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_validating_a_luggage_store_exists.cs @@ -32,10 +32,10 @@ public class S3LuggageStoreExistsTests public S3LuggageStoreExistsTests() { //arrange - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); - - _client = new AmazonS3Client(credentials, region); - _stsClient = new AmazonSecurityTokenServiceClient(credentials, region); + var factory = new AWSClientFactory(GatewayFactory.CreateFactory()); + _client = factory.CreateS3Client(); + _stsClient = factory.CreateStsClient(); + var services = new ServiceCollection(); services.AddHttpClient(); diff --git a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs index 03450a5fd3..fa0ab9db5b 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs +++ b/tests/Paramore.Brighter.AWS.Tests/Transformers/When_wrapping_a_large_message.cs @@ -42,10 +42,10 @@ public LargeMessagePayloadWrapTests() _myCommand = new MyLargeCommand(6000); - (AWSCredentials credentials, RegionEndpoint region) = CredentialsChain.GetAwsCredentials(); + var factory = new AWSClientFactory(GatewayFactory.CreateFactory()); - _client = new AmazonS3Client(credentials, region); - AmazonSecurityTokenServiceClient stsClient = new(credentials, region); + _client = factory.CreateS3Client(); + var stsClient = factory.CreateStsClient(); var services = new ServiceCollection(); services.AddHttpClient(); @@ -64,7 +64,7 @@ public LargeMessagePayloadWrapTests() #pragma warning disable CS0618 // It is obsolete, but we want the string value here not the replacement one bucketRegion: S3Region.EUW1, #pragma warning restore CS0618 - tags: new List { new Tag { Key = "BrighterTests", Value = "S3LuggageUploadTests" } }, + tags: [new Tag { Key = "BrighterTests", Value = "S3LuggageUploadTests" }], acl: S3CannedACL.Private, abortFailedUploadsAfterDays: 1, deleteGoodUploadsAfterDays: 1)