diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 496cd9f21..e1f122658 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -52,6 +52,7 @@ jobs: { name: "Testcontainers.DynamoDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Elasticsearch", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.EventStoreDb", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.EventHubs", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.FakeGcsServer", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.FirebirdSql", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Firestore", runs-on: "ubuntu-22.04" }, diff --git a/Directory.Packages.props b/Directory.Packages.props index 82dda0fd4..0a4db1cff 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,6 +32,7 @@ + diff --git a/Testcontainers.sln b/Testcontainers.sln index 06e1381fc..96eb3545f 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DynamoDb", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearch", "src\Testcontainers.Elasticsearch\Testcontainers.Elasticsearch.csproj", "{641DDEA5-B6E0-41E6-BA11-7A28C0913127}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventHubs", "src\Testcontainers.EventHubs\Testcontainers.EventHubs.csproj", "{0EF885E9-E973-47DC-AA9C-3A5E9175B0F3}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb", "src\Testcontainers.EventStoreDb\Testcontainers.EventStoreDb.csproj", "{84D707E0-C9FA-4327-85DC-0AFEBEA73572}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer", "src\Testcontainers.FakeGcsServer\Testcontainers.FakeGcsServer.csproj", "{FF86B509-2F9E-4269-ABC2-912B3339DE29}" @@ -143,6 +145,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DynamoDb.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Elasticsearch.Tests", "tests\Testcontainers.Elasticsearch.Tests\Testcontainers.Elasticsearch.Tests.csproj", "{DD5B3678-468F-4D73-AECE-705E3D66CD43}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventHubs.Tests", "tests\Testcontainers.EventHubs.Tests\Testcontainers.EventHubs.Tests.csproj", "{4A0C5523-CEB2-49C9-AE62-9187A01B016B}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb.Tests", "tests\Testcontainers.EventStoreDb.Tests\Testcontainers.EventStoreDb.Tests.csproj", "{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer.Tests", "tests\Testcontainers.FakeGcsServer.Tests\Testcontainers.FakeGcsServer.Tests.csproj", "{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}" @@ -282,6 +286,10 @@ Global {641DDEA5-B6E0-41E6-BA11-7A28C0913127}.Debug|Any CPU.Build.0 = Debug|Any CPU {641DDEA5-B6E0-41E6-BA11-7A28C0913127}.Release|Any CPU.ActiveCfg = Release|Any CPU {641DDEA5-B6E0-41E6-BA11-7A28C0913127}.Release|Any CPU.Build.0 = Release|Any CPU + {0EF885E9-E973-47DC-AA9C-3A5E9175B0F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0EF885E9-E973-47DC-AA9C-3A5E9175B0F3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0EF885E9-E973-47DC-AA9C-3A5E9175B0F3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0EF885E9-E973-47DC-AA9C-3A5E9175B0F3}.Release|Any CPU.Build.0 = Release|Any CPU {84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Debug|Any CPU.Build.0 = Debug|Any CPU {84D707E0-C9FA-4327-85DC-0AFEBEA73572}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -486,6 +494,10 @@ Global {DD5B3678-468F-4D73-AECE-705E3D66CD43}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD5B3678-468F-4D73-AECE-705E3D66CD43}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD5B3678-468F-4D73-AECE-705E3D66CD43}.Release|Any CPU.Build.0 = Release|Any CPU + {4A0C5523-CEB2-49C9-AE62-9187A01B016B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A0C5523-CEB2-49C9-AE62-9187A01B016B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A0C5523-CEB2-49C9-AE62-9187A01B016B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A0C5523-CEB2-49C9-AE62-9187A01B016B}.Release|Any CPU.Build.0 = Release|Any CPU {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -650,6 +662,7 @@ Global {ED3C611F-DFE2-4AB7-A323-B500E95B4FF9} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {2EAFA567-9F68-4C52-9DBC-8F3EC11BB2CE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {641DDEA5-B6E0-41E6-BA11-7A28C0913127} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {0EF885E9-E973-47DC-AA9C-3A5E9175B0F3} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {FF86B509-2F9E-4269-ABC2-912B3339DE29} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {31BAF2C4-0608-4C0F-845A-14FE7C0A1670} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -701,6 +714,7 @@ Global {AF9853AB-86E7-49DE-8DF8-454838E90D6F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {101515E6-74C1-40F9-85C8-871F742A378D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {DD5B3678-468F-4D73-AECE-705E3D66CD43} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {4A0C5523-CEB2-49C9-AE62-9187A01B016B} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E39095AC-9B34-4178-A486-04C902B6FD33} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/docs/modules/index.md b/docs/modules/index.md index 48a75587b..6e6681edb 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -24,6 +24,7 @@ await moduleNameContainer.StartAsync(); | ActiveMQ Artemis | `apache/activemq-artemis:2.31.2` | [NuGet](https://www.nuget.org/packages/Testcontainers.ActiveMq) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ActiveMq) | | ArangoDB | `arangodb:3.11.5` | [NuGet](https://www.nuget.org/packages/Testcontainers.ArangoDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ArangoDb) | | Azure Cosmos DB | `mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.CosmosDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.CosmosDb) | +| Azure Event Hubs | `mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.EventHubs) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.EventHubs) | | Azure Service Bus | `mcr.microsoft.com/azure-messaging/servicebus-emulator:latest` | [NuGet](https://www.nuget.org/packages/Testcontainers.ServiceBus) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ServiceBus) | | Azurite | `mcr.microsoft.com/azure-storage/azurite:3.24.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Azurite) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Azurite) | | BigQuery | `ghcr.io/goccy/bigquery-emulator:0.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.BigQuery) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.BigQuery) | diff --git a/src/Testcontainers.Azurite/AzuriteContainer.cs b/src/Testcontainers.Azurite/AzuriteContainer.cs index e0cbd0d1b..b7b1023b7 100644 --- a/src/Testcontainers.Azurite/AzuriteContainer.cs +++ b/src/Testcontainers.Azurite/AzuriteContainer.cs @@ -32,30 +32,27 @@ public string GetConnectionString() /// /// Gets the blob endpoint /// - /// The azurite blob endpoint + /// The Azurite blob endpoint public string GetBlobEndpoint() { - return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.BlobPort), - AzuriteBuilder.AccountName).ToString(); + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.BlobPort), AzuriteBuilder.AccountName).ToString(); } /// /// Gets the queue endpoint /// - /// The azurite queue endpoint + /// The Azurite queue endpoint public string GetQueueEndpoint() { - return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.QueuePort), - AzuriteBuilder.AccountName).ToString(); + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.QueuePort), AzuriteBuilder.AccountName).ToString(); } /// /// Gets the table endpoint /// - /// The azurite table endpoint + /// The Azurite table endpoint public string GetTableEndpoint() { - return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.TablePort), - AzuriteBuilder.AccountName).ToString(); + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(AzuriteBuilder.TablePort), AzuriteBuilder.AccountName).ToString(); } } \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/.editorconfig b/src/Testcontainers.EventHubs/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.EventHubs/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/EventHubsBuilder.cs b/src/Testcontainers.EventHubs/EventHubsBuilder.cs new file mode 100644 index 000000000..a24bd217a --- /dev/null +++ b/src/Testcontainers.EventHubs/EventHubsBuilder.cs @@ -0,0 +1,178 @@ +namespace Testcontainers.EventHubs; + +/// +[PublicAPI] +public sealed class EventHubsBuilder : ContainerBuilder +{ + public const string EventHubsNetworkAlias = "eventhubs-container"; + + public const string AzuriteNetworkAlias = "azurite-container"; + + public const string EventHubsImage = "mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest"; + + public const ushort EventHubsPort = 5672; + + /// + /// Initializes a new instance of the class. + /// + public EventHubsBuilder() + : this(new EventHubsConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private EventHubsBuilder(EventHubsConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override EventHubsConfiguration DockerResourceConfiguration { get; } + + /// + protected override string AcceptLicenseAgreementEnvVar { get; } = "ACCEPT_EULA"; + + /// + protected override string AcceptLicenseAgreement { get; } = "Y"; + + /// + protected override string DeclineLicenseAgreement { get; } = "N"; + + /// + /// Accepts the license agreement. + /// + /// + /// When is set to true, the Azure Event Hubs Emulator license is accepted. + /// + /// A boolean value indicating whether the Azure Event Hubs Emulator license agreement is accepted. + /// A configured instance of . + public override EventHubsBuilder WithAcceptLicenseAgreement(bool acceptLicenseAgreement) + { + var licenseAgreement = acceptLicenseAgreement ? AcceptLicenseAgreement : DeclineLicenseAgreement; + return WithEnvironment(AcceptLicenseAgreementEnvVar, licenseAgreement); + } + + /// + /// Sets the dependent Azurite container for the Azure Event Hubs Emulator. + /// + /// + /// This method allows an existing Azurite container to be attached to the Azure Event + /// Hubs Emulator. The containers must be on the same network to enable communication + /// between them. + /// + /// The network to connect the container to. + /// The Azurite container. + /// The Azurite container network alias. + /// A configured instance of . + public EventHubsBuilder WithAzuriteContainer( + INetwork network, + AzuriteContainer container, + string networkAlias) + { + return Merge(DockerResourceConfiguration, new EventHubsConfiguration(azuriteContainer: container)) + .DependsOn(container) + .WithNetwork(network) + .WithNetworkAliases(EventHubsNetworkAlias) + .WithEnvironment("BLOB_SERVER", networkAlias) + .WithEnvironment("METADATA_SERVER", networkAlias); + } + + /// + /// Sets the Azure Event Hubs Emulator configuration. + /// + /// The service configuration. + /// A configured instance of . + public EventHubsBuilder WithConfigurationBuilder(EventHubsServiceConfiguration serviceConfiguration) + { + var resourceContent = Encoding.Default.GetBytes(serviceConfiguration.Build()); + return Merge(DockerResourceConfiguration, new EventHubsConfiguration(serviceConfiguration: serviceConfiguration)) + .WithResourceMapping(resourceContent, "Eventhubs_Emulator/ConfigFiles/Config.json"); + } + + /// + public override EventHubsContainer Build() + { + Validate(); + ValidateLicenseAgreement(); + + if (DockerResourceConfiguration.AzuriteContainer != null) + { + return new EventHubsContainer(DockerResourceConfiguration); + } + + // If the user has not provided an existing Azurite container instance, + // we configure one. + var network = new NetworkBuilder() + .Build(); + + var container = new AzuriteBuilder() + .WithNetwork(network) + .WithNetworkAliases(AzuriteNetworkAlias) + .Build(); + + var eventHubsContainer = WithAzuriteContainer(network, container, AzuriteNetworkAlias); + return new EventHubsContainer(eventHubsContainer.DockerResourceConfiguration); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.ServiceConfiguration, nameof(DockerResourceConfiguration.ServiceConfiguration)) + .NotNull() + .ThrowIf(argument => !argument.Value.Validate(), _ => throw new ArgumentException("The service configuration of the Azure Event Hubs Emulator is invalid.")); + } + + /// + protected override EventHubsBuilder Init() + { + return base.Init() + .WithImage(EventHubsImage) + .WithPortBinding(EventHubsPort, true) + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilMessageIsLogged("Emulator Service is Successfully Up!") + .AddCustomWaitStrategy(new WaitTwoSeconds())); + } + + /// + protected override EventHubsBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new EventHubsConfiguration(resourceConfiguration)); + } + + /// + protected override EventHubsBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new EventHubsConfiguration(resourceConfiguration)); + } + + /// + protected override EventHubsBuilder Merge(EventHubsConfiguration oldValue, EventHubsConfiguration newValue) + { + return new EventHubsBuilder(new EventHubsConfiguration(oldValue, newValue)); + } + + /// + /// + /// This is a workaround to ensure that the wait strategy does not indicate + /// readiness too early: + /// https://github.com/Azure/azure-service-bus-emulator-installer/issues/35#issuecomment-2497164533. + /// + private sealed class WaitTwoSeconds : IWaitUntil + { + /// + public async Task UntilAsync(IContainer container) + { + await Task.Delay(TimeSpan.FromSeconds(2)) + .ConfigureAwait(false); + + return true; + } + } +} \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/EventHubsConfiguration.cs b/src/Testcontainers.EventHubs/EventHubsConfiguration.cs new file mode 100644 index 000000000..76c57ecbb --- /dev/null +++ b/src/Testcontainers.EventHubs/EventHubsConfiguration.cs @@ -0,0 +1,70 @@ +namespace Testcontainers.EventHubs; + +/// +[PublicAPI] +public sealed class EventHubsConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The Azurite container. + /// The service configuration. + public EventHubsConfiguration(AzuriteContainer azuriteContainer = null, + EventHubsServiceConfiguration serviceConfiguration = null) + { + AzuriteContainer = azuriteContainer; + ServiceConfiguration = serviceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public EventHubsConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public EventHubsConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public EventHubsConfiguration(EventHubsConfiguration resourceConfiguration) + : this(new EventHubsConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public EventHubsConfiguration(EventHubsConfiguration oldValue, EventHubsConfiguration newValue) + : base(oldValue, newValue) + { + AzuriteContainer = BuildConfiguration.Combine(oldValue.AzuriteContainer, newValue.AzuriteContainer); + ServiceConfiguration = BuildConfiguration.Combine(oldValue.ServiceConfiguration, newValue.ServiceConfiguration); + } + + /// + /// Gets the Azurite container. + /// + public AzuriteContainer AzuriteContainer { get; } + + /// + /// Gets the service configuration. + /// + public EventHubsServiceConfiguration ServiceConfiguration { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/EventHubsContainer.cs b/src/Testcontainers.EventHubs/EventHubsContainer.cs new file mode 100644 index 000000000..c575aec21 --- /dev/null +++ b/src/Testcontainers.EventHubs/EventHubsContainer.cs @@ -0,0 +1,33 @@ +namespace Testcontainers.EventHubs; + +/// +[PublicAPI] +public sealed class EventHubsContainer : DockerContainer +{ + private readonly EventHubsConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public EventHubsContainer(EventHubsConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the Event Hubs connection string. + /// + /// The Event Hubs connection string. + public string GetConnectionString() + { + var properties = new Dictionary(); + properties.Add("Endpoint", new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(EventHubsBuilder.EventHubsPort)).ToString()); + properties.Add("DefaultEndpointsProtocol", Uri.UriSchemeHttp); + properties.Add("SharedAccessKeyName", "RootManageSharedAccessKey"); + properties.Add("SharedAccessKey", "SAS_KEY_VALUE"); + properties.Add("UseDevelopmentEmulator", "true"); + return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); + } +} \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/EventHubsServiceConfiguration.cs b/src/Testcontainers.EventHubs/EventHubsServiceConfiguration.cs new file mode 100644 index 000000000..120ff65d6 --- /dev/null +++ b/src/Testcontainers.EventHubs/EventHubsServiceConfiguration.cs @@ -0,0 +1,89 @@ +namespace Testcontainers.EventHubs; + +[PublicAPI] +public record RootConfiguration(UserConfig UserConfig) +{ + public UserConfig UserConfig { get; } = UserConfig; +} + +[PublicAPI] +public record UserConfig(IReadOnlyCollection NamespaceConfig, LoggingConfig LoggingConfig) +{ + public IReadOnlyCollection NamespaceConfig { get; } = NamespaceConfig; + + public LoggingConfig LoggingConfig { get; } = LoggingConfig; +} + +[PublicAPI] +public record NamespaceConfig(string Type, string Name, IReadOnlyCollection Entities) +{ + public string Type { get; } = Type; + + public string Name { get; } = Name; + + public IReadOnlyCollection Entities { get; } = Entities; +} + +[PublicAPI] +public record Entity(string Name, int PartitionCount, IReadOnlyCollection ConsumerGroups) +{ + public string Name { get; } = Name; + + public int PartitionCount { get; } = PartitionCount; + + public IReadOnlyCollection ConsumerGroups { get; } = ConsumerGroups; +} + +[PublicAPI] +public record ConsumerGroup(string Name) +{ + public string Name { get; } = Name; +} + +[PublicAPI] +public record LoggingConfig(string Type) +{ + public string Type { get; } = Type; +} + +[PublicAPI] +public sealed class EventHubsServiceConfiguration +{ + private readonly NamespaceConfig _namespaceConfig; + + private EventHubsServiceConfiguration(NamespaceConfig namespaceConfig) + { + _namespaceConfig = namespaceConfig; + } + + public static EventHubsServiceConfiguration Create() + { + var namespaceConfig = new NamespaceConfig("EventHub", "ns-1", Array.Empty()); + return new EventHubsServiceConfiguration(namespaceConfig); + } + + public EventHubsServiceConfiguration WithEntity(string name, int partitionCount, params string[] consumerGroupNames) + { + return WithEntity(name, partitionCount, new ReadOnlyCollection(consumerGroupNames)); + } + + public EventHubsServiceConfiguration WithEntity(string name, int partitionCount, IEnumerable consumerGroupNames) + { + var consumerGroups = new ReadOnlyCollection(consumerGroupNames.Select(consumerGroupName => new ConsumerGroup(consumerGroupName)).ToList()); + var entity = new Entity(name, partitionCount, consumerGroups); + var entities = new ReadOnlyCollection(_namespaceConfig.Entities.Append(entity).ToList()); + return new EventHubsServiceConfiguration(new NamespaceConfig(_namespaceConfig.Type, _namespaceConfig.Name, entities)); + } + + public bool Validate() + { + Predicate isValidEntity = entity => entity.PartitionCount > 0 && entity.PartitionCount <= 32 && entity.ConsumerGroups.Count > 0 && entity.ConsumerGroups.Count <= 20; + return _namespaceConfig.Entities.All(entity => isValidEntity(entity)); + } + + public string Build() + { + var rootConfiguration = new RootConfiguration(new UserConfig([_namespaceConfig], new LoggingConfig("file"))); + return JsonSerializer.Serialize(rootConfiguration); + } +} \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/Testcontainers.EventHubs.csproj b/src/Testcontainers.EventHubs/Testcontainers.EventHubs.csproj new file mode 100644 index 000000000..643fdade3 --- /dev/null +++ b/src/Testcontainers.EventHubs/Testcontainers.EventHubs.csproj @@ -0,0 +1,13 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.EventHubs/Usings.cs b/src/Testcontainers.EventHubs/Usings.cs new file mode 100644 index 000000000..b68f67969 --- /dev/null +++ b/src/Testcontainers.EventHubs/Usings.cs @@ -0,0 +1,15 @@ +global using System; +global using System.Collections.Generic; +global using System.Collections.ObjectModel; +global using System.Linq; +global using System.Text; +global using System.Text.Json; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using DotNet.Testcontainers.Networks; +global using JetBrains.Annotations; +global using Testcontainers.Azurite; \ No newline at end of file diff --git a/tests/Testcontainers.EventHubs.Tests/.editorconfig b/tests/Testcontainers.EventHubs.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.EventHubs.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs b/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs new file mode 100644 index 000000000..619c5f642 --- /dev/null +++ b/tests/Testcontainers.EventHubs.Tests/EventHubsContainerTest.cs @@ -0,0 +1,101 @@ +namespace Testcontainers.EventHubs; + +public abstract class EventHubsContainerTest : IAsyncLifetime +{ + private const string EventHubsName = "eh-1"; + + private const string EventHubsConsumerGroupName = "cg-1"; + + private readonly EventHubsContainer _eventHubsContainer; + + private EventHubsContainerTest(EventHubsContainer eventHubsContainer) + { + _eventHubsContainer = eventHubsContainer; + } + + public Task InitializeAsync() + { + return _eventHubsContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _eventHubsContainer.DisposeAsync().AsTask(); + } + + private static EventHubsServiceConfiguration GetServiceConfiguration() + { + return EventHubsServiceConfiguration.Create().WithEntity(EventHubsName, 2, EventHubsConsumerGroupName); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task SendEventDataBatchShouldNotThrowException() + { + // Given + var message = Guid.NewGuid().ToString(); + + await using var client = new EventHubProducerClient(_eventHubsContainer.GetConnectionString(), EventHubsName); + + // When + var properties = await client.GetEventHubPropertiesAsync() + .ConfigureAwait(true); + + using var eventDataBatch = await client.CreateBatchAsync() + .ConfigureAwait(true); + + eventDataBatch.TryAdd(new EventData(message)); + + await client.SendAsync(eventDataBatch) + .ConfigureAwait(true); + + // Then + Assert.NotNull(properties); + } + + [UsedImplicitly] + public sealed class EventHubsDefaultAzuriteConfiguration : EventHubsContainerTest + { + public EventHubsDefaultAzuriteConfiguration() + : base(new EventHubsBuilder() + .WithAcceptLicenseAgreement(true) + .WithConfigurationBuilder(GetServiceConfiguration()) + .Build()) + { + } + } + + [UsedImplicitly] + public sealed class EventHubsCustomAzuriteConfiguration : EventHubsContainerTest, IClassFixture + { + public EventHubsCustomAzuriteConfiguration(DatabaseFixture fixture) + : base(new EventHubsBuilder() + .WithAcceptLicenseAgreement(true) + .WithConfigurationBuilder(GetServiceConfiguration()) + .WithAzuriteContainer(fixture.Network, fixture.Container, DatabaseFixture.AzuriteNetworkAlias) + .Build()) + { + } + } + + [UsedImplicitly] + public sealed class DatabaseFixture + { + public DatabaseFixture() + { + Network = new NetworkBuilder() + .Build(); + + Container = new AzuriteBuilder() + .WithNetwork(Network) + .WithNetworkAliases(AzuriteNetworkAlias) + .Build(); + } + + public static string AzuriteNetworkAlias => EventHubsBuilder.AzuriteNetworkAlias; + + public INetwork Network { get; } + + public AzuriteContainer Container { get; } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj b/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj new file mode 100644 index 000000000..f797bf3b5 --- /dev/null +++ b/tests/Testcontainers.EventHubs.Tests/Testcontainers.EventHubs.Tests.csproj @@ -0,0 +1,18 @@ + + + net9.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.EventHubs.Tests/Usings.cs b/tests/Testcontainers.EventHubs.Tests/Usings.cs new file mode 100644 index 000000000..371cf2494 --- /dev/null +++ b/tests/Testcontainers.EventHubs.Tests/Usings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.Threading.Tasks; +global using Azure.Messaging.EventHubs; +global using Azure.Messaging.EventHubs.Producer; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Commons; +global using DotNet.Testcontainers.Networks; +global using JetBrains.Annotations; +global using Testcontainers.Azurite; +global using Xunit; \ No newline at end of file diff --git a/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs b/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs index 685d8dd3e..5944fae3f 100644 --- a/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs +++ b/tests/Testcontainers.ServiceBus.Tests/ServiceBusContainerTest.cs @@ -57,7 +57,9 @@ await sender.SendMessageAsync(message) public sealed class ServiceBusDefaultMsSqlConfiguration : ServiceBusContainerTest { public ServiceBusDefaultMsSqlConfiguration() - : base(new ServiceBusBuilder().WithAcceptLicenseAgreement(true).Build()) + : base(new ServiceBusBuilder() + .WithAcceptLicenseAgreement(true) + .Build()) { } } @@ -66,7 +68,10 @@ public ServiceBusDefaultMsSqlConfiguration() public sealed class ServiceBusCustomMsSqlConfiguration : ServiceBusContainerTest, IClassFixture { public ServiceBusCustomMsSqlConfiguration(DatabaseFixture fixture) - : base(new ServiceBusBuilder().WithAcceptLicenseAgreement(true).WithMsSqlContainer(fixture.Network, fixture.Container, DatabaseFixture.DatabaseNetworkAlias).Build()) + : base(new ServiceBusBuilder() + .WithAcceptLicenseAgreement(true) + .WithMsSqlContainer(fixture.Network, fixture.Container, DatabaseFixture.DatabaseNetworkAlias) + .Build()) { } }