-
Notifications
You must be signed in to change notification settings - Fork 18
Add Azure ServiceBus Emulator #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <Import Project="$(CCTestProjectProps)" Condition="Exists('$(CCTestProjectProps)')"/> | ||
| <PropertyGroup> | ||
| <RootNamespace>Squadron.AzureServiceBus.Tests</RootNamespace> | ||
| <AssemblyName>Squadron.AzureServiceBus.Tests</AssemblyName> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\AzureServiceBus\AzureServiceBus.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <None Update="xunit.runner.json"> | ||
| <CopyToOutputDirectory>Always</CopyToOutputDirectory> | ||
| </None> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
| using Azure.Messaging.ServiceBus; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace Squadron.AzureServiceBus.Tests; | ||
|
|
||
| public class AzureServiceBusCustomConfigTests( | ||
| AzureServiceBusResources<CustomConfig> azureServiceBusResource) : | ||
| IClassFixture<AzureServiceBusResources<CustomConfig>> | ||
| { | ||
| [Fact] | ||
| public async Task Send_And_Receive() | ||
| { | ||
| var sender = azureServiceBusResource.Client | ||
| .CreateSender("custom.topic"); | ||
| var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("custom_message")); | ||
| await sender.SendMessageAsync(serviceBusMessage); | ||
|
|
||
| var receiver = azureServiceBusResource.Client | ||
| .CreateReceiver("custom.queue"); | ||
| var message = await receiver.ReceiveMessageAsync(); | ||
| var receivedMessage = message.Body.ToString(); | ||
|
|
||
| receivedMessage.Should().Be("custom_message"); | ||
| } | ||
| } | ||
|
|
||
| public class CustomConfig : AzureServiceBusConfig | ||
| { | ||
| protected override Queue[] CreateQueues() | ||
| { | ||
| return [new Queue("custom.queue")]; | ||
| } | ||
|
|
||
| protected override Topic[] CreateTopics() | ||
| { | ||
| var subscription = new Subscription( | ||
| "custom.subscription", | ||
| new SubscriptionProperties(ForwardTo: "custom.queue")); | ||
|
|
||
| return [new Topic("custom.topic", Subscriptions: [subscription])]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace Squadron.AzureServiceBus.Tests; | ||
|
|
||
| public class AzureServiceBusResourceTests( | ||
| AzureServiceBusResources azureServiceBusResource) : | ||
| IClassFixture<AzureServiceBusResources> | ||
| { | ||
| [Fact] | ||
| public async Task Send_And_Receive() | ||
| { | ||
| var receiver = azureServiceBusResource.Client | ||
| .CreateReceiver(AzureServiceBusStatus.QueueName); | ||
| var message = await receiver.ReceiveMessageAsync(); | ||
| var receivedMessage = message.Body.ToString(); | ||
|
|
||
| receivedMessage.Should().Be("status_check"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "appDomain": "denied", | ||
| "parallelizeAssembly": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <Import Project="$(CCResourceProjectProps)" Condition="Exists('$(CCResourceProjectProps)')" /> | ||
|
|
||
| <PropertyGroup> | ||
| <AssemblyName>Squadron.AzureServiceBus</AssemblyName> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.0" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\Compose\Compose.csproj" /> | ||
| <ProjectReference Include="..\Core\Core.csproj" /> | ||
| <ProjectReference Include="..\SqlServer\SqlServer.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace Squadron; | ||
|
|
||
| public class AzureServiceBusConfig | ||
| { | ||
| private static readonly JsonSerializerOptions _options = new() | ||
| { | ||
| DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull | ||
| }; | ||
|
|
||
| protected virtual Queue[] CreateQueues() | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| protected virtual Topic[] CreateTopics() | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| public string Build() | ||
| { | ||
| var fileName = Path.GetTempFileName(); | ||
|
|
||
| Queue[] queues = CreateQueues() | ||
| .Concat([new Queue(AzureServiceBusStatus.QueueName)]) | ||
| .ToArray(); | ||
|
|
||
| var config = new | ||
| { | ||
| UserConfig = new UserConfig( | ||
| [new Namespace(queues, CreateTopics())], | ||
| new Logging()) | ||
| }; | ||
|
|
||
| var serializedConfig = JsonSerializer.Serialize(config, options: _options); | ||
| File.WriteAllText(fileName, serializedConfig); | ||
|
|
||
| return fileName; | ||
| } | ||
| } | ||
|
|
||
| public record UserConfig( | ||
| Namespace[] Namespaces, | ||
| Logging Logging); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer a line break between the records. |
||
| public record Namespace( | ||
| Queue[] Queues, | ||
| Topic[] Topics, | ||
| string Name = "sbemulatorns"); | ||
| public record Queue( | ||
| string Name, | ||
| QueueProperties? Properties = null); | ||
| public record QueueProperties( | ||
| bool DeadLetteringOnMessageExpiration = true, | ||
| string DefaultMessageTimeToLive= "PT1H", | ||
| string DuplicateDetectionHistoryTimeWindow = "PT1M", | ||
| string ForwardDeadLetteredMessagesTo = "", | ||
| string ForwardTo = "", | ||
| string LockDuration= "PT1M", | ||
| int MaxDeliveryCount = 5, | ||
| bool RequiresDuplicateDetection = false, | ||
| bool RequiresSession = false); | ||
| public record Topic( | ||
| string Name, | ||
| TopicProperties? Properties = null, | ||
| Subscription[]? Subscriptions = null); | ||
| public record TopicProperties( | ||
| string DefaultMessageTimeToLive = "PT1H", | ||
| string DuplicateDetectionHistoryTimeWindow = "PT1M", | ||
| bool RequiresDuplicateDetection = false); | ||
| public record Subscription( | ||
| string Name, | ||
| SubscriptionProperties? Properties = null, | ||
| SubscriptionRule[]? Rules = null); | ||
| public record SubscriptionProperties( | ||
| bool DeadLetteringOnMessageExpiration = true, | ||
| string DefaultMessageTimeToLive = "PT1H", | ||
| string LockDuration = "PT1M", | ||
| int MaxDeliveryCount = 5, | ||
| string ForwardDeadLetteredMessagesTo = "", | ||
| string ForwardTo = "", | ||
| bool RequiresSession = false); | ||
| public record SubscriptionRule( | ||
| string Name, | ||
| TopicRuleProperties? Properties = null); | ||
| public record TopicRuleProperties( | ||
| string FilterType, | ||
| CorrelationFilter CorrelationFilter); | ||
| public record CorrelationFilter( | ||
| string ContentType, | ||
| string CorrelationId, | ||
| string Label, | ||
| string MessageId, | ||
| string ReplyTo, | ||
| string ReplyToSessionId, | ||
| string SessionId, | ||
| string To); | ||
| public record Logging(string Type = "Console"); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| namespace Squadron; | ||
|
|
||
| internal class AzureServiceBusConstants | ||
| { | ||
| internal static string SqlServerResourceName { get; } = "asb_sql_server"; | ||
| internal static string AzureServiceBusEmulatorResourceName { get; } = "asb_emulator"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| namespace Squadron; | ||
|
|
||
| public class AzureServiceBusDefaultOptions<TConfig> : ComposeResourceOptions | ||
| where TConfig : AzureServiceBusConfig, new() | ||
| { | ||
| public override void Configure(ComposeResourceBuilder builder) | ||
| { | ||
| builder.AddContainer<SqlServerDefaultOptions>( | ||
| AzureServiceBusConstants.SqlServerResourceName); | ||
|
|
||
| builder | ||
| .AddContainer<AzureServiceBusEmulatorDefaultOptions<TConfig>>( | ||
| AzureServiceBusConstants.AzureServiceBusEmulatorResourceName) | ||
| .AddLink(AzureServiceBusConstants.SqlServerResourceName, | ||
| new EnvironmentVariableMapping("SQL_SERVER", "#NAME#"), | ||
| new EnvironmentVariableMapping("MSSQL_SA_PASSWORD", "#MSSQL_SA_PASSWORD#")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| using System; | ||
|
|
||
| namespace Squadron; | ||
|
|
||
| public class AzureServiceBusEmulatorDefaultOptions<TConfig> : | ||
| ContainerResourceOptions, | ||
| IComposableResourceOption | ||
| where TConfig : AzureServiceBusConfig, new() | ||
| { | ||
| public Type ResourceType => typeof(AzureServiceBusEmulatorResource<TConfig>); | ||
|
|
||
| /// <summary> | ||
| /// Configure resource options for AzureServiceBusEmulatorDefaultOptions | ||
| /// </summary> | ||
| /// <param name="builder"></param> | ||
| public override void Configure(ContainerResourceBuilder builder) | ||
| { | ||
| var azureServiceBusConfig = new TConfig(); | ||
| var configFile = azureServiceBusConfig.Build(); | ||
|
|
||
| builder | ||
| .Name("asb_emulator") | ||
| .Image("mcr.microsoft.com/azure-messaging/servicebus-emulator") | ||
| .InternalPort(5672) | ||
| .WaitTimeout(120) | ||
| .AddEnvironmentVariable("ACCEPT_EULA=Y") | ||
| .AddVolume($"{configFile}:/ServiceBus_Emulator/ConfigFiles/Config.json") | ||
| .PreferLocalImage(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| using System; | ||
| using System.Threading.Tasks; | ||
| using Azure.Messaging.ServiceBus; | ||
| using Azure.Messaging.ServiceBus.Administration; | ||
| using Xunit; | ||
|
|
||
| namespace Squadron; | ||
|
|
||
| /// <inheritdoc/> | ||
| public class AzureServiceBusEmulatorResource<TConfig> | ||
| : AzureServiceBusEmulatorResource<AzureServiceBusEmulatorDefaultOptions<TConfig>, TConfig> | ||
| where TConfig : AzureServiceBusConfig, new(); | ||
|
|
||
| /// <summary> | ||
| /// Represents an AzureServiceBus resource that can be used by unit tests. | ||
| /// </summary> | ||
| /// <seealso cref="IDisposable"/> | ||
| public class AzureServiceBusEmulatorResource<TOptions, TConfig> : | ||
| ContainerResource<TOptions>, | ||
| IAsyncLifetime, | ||
| IComposableResource | ||
| where TOptions : ContainerResourceOptions, new() | ||
| { | ||
| /// <summary> | ||
| /// Connection string to access the Azure Service Bus | ||
| /// </summary> | ||
| public string ConnectionString { get; private set; } | ||
|
|
||
| /// <summary> | ||
| /// Azure Service Bus client | ||
| /// </summary> | ||
| public ServiceBusClient Client { get; private set; } | ||
|
|
||
| /// <inheritdoc cref="IAsyncLifetime"/> | ||
| public override async Task InitializeAsync() | ||
| { | ||
| await base.InitializeAsync(); | ||
|
|
||
| ConnectionString = | ||
| $"Endpoint=sb://{Manager.Instance.Address}:{Manager.Instance.HostPort}" + | ||
| ";SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; | ||
|
|
||
| Client = new ServiceBusClient(ConnectionString); | ||
| await Initializer.WaitAsync(new AzureServiceBusStatus(Client)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| using Azure.Messaging.ServiceBus; | ||
| using SharpCompress; | ||
|
|
||
| namespace Squadron; | ||
|
|
||
| public class AzureServiceBusResources : | ||
| AzureServiceBusResources<AzureServiceBusConfig>; | ||
|
|
||
| public class AzureServiceBusResources<TConfig> : | ||
| ComposeResource<AzureServiceBusDefaultOptions<TConfig>> | ||
| where TConfig : AzureServiceBusConfig, new() | ||
| { | ||
| private readonly Lazy<AzureServiceBusEmulatorResource<TConfig>> _azureServiceBusEmulatorResource; | ||
|
|
||
| public AzureServiceBusResources() | ||
| { | ||
| _azureServiceBusEmulatorResource = new Lazy<AzureServiceBusEmulatorResource<TConfig>>(() => | ||
| (Managers[AzureServiceBusConstants.AzureServiceBusEmulatorResourceName].Resource | ||
| as AzureServiceBusEmulatorResource<TConfig>)!); | ||
| } | ||
|
|
||
| public string ConnectionString => _azureServiceBusEmulatorResource.Value.ConnectionString!; | ||
|
|
||
| public ServiceBusClient Client => _azureServiceBusEmulatorResource.Value.Client!; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| using System; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Azure.Messaging.ServiceBus; | ||
|
|
||
| namespace Squadron; | ||
|
|
||
| public class AzureServiceBusStatus(ServiceBusClient client) : | ||
| IResourceStatusProvider | ||
| { | ||
| public static string QueueName = "squadron.status.check"; | ||
|
|
||
| public async Task<Status> IsReadyAsync(CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var sender = client.CreateSender(QueueName); | ||
| var serviceBusMessage = new ServiceBusMessage(BinaryData.FromString("status_check")); | ||
| await sender.SendMessageAsync(serviceBusMessage, cancellationToken); | ||
|
|
||
| return new Status { IsReady = true }; | ||
| } | ||
| catch (Exception ex) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would somehow not swallow the exception, is there a possibility to add it to the Status, or so, at least the message? |
||
| { | ||
| return new Status { IsReady = false, Message = "Not ready" }; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have no central package management?