diff --git a/Testcontainers.sln b/Testcontainers.sln index 813d4d211..6c664505d 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Oracle", "sr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql", "src\Testcontainers.PostgreSql\Testcontainers.PostgreSql.csproj", "{8AB91636-9055-4900-A72A-7CFFACDFDBF0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub", "src\Testcontainers.PubSub\Testcontainers.PubSub.csproj", "{E6642255-667D-476B-B584-089AA5E6C0B1}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq", "src\Testcontainers.RabbitMq\Testcontainers.RabbitMq.csproj", "{A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb", "src\Testcontainers.RavenDb\Testcontainers.RavenDb.csproj", "{F6394475-D6F1-46E2-81BF-4BA78A40B878}" @@ -131,6 +133,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Platform.Win EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PostgreSql.Tests", "tests\Testcontainers.PostgreSql.Tests\Testcontainers.PostgreSql.Tests.csproj", "{56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.PubSub.Tests", "tests\Testcontainers.PubSub.Tests\Testcontainers.PubSub.Tests.csproj", "{0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RabbitMq.Tests", "tests\Testcontainers.RabbitMq.Tests\Testcontainers.RabbitMq.Tests.csproj", "{19564567-1736-4626-B406-17E4E02F18B2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.RavenDb.Tests", "tests\Testcontainers.RavenDb.Tests\Testcontainers.RavenDb.Tests.csproj", "{D53726B6-5447-47E6-B881-A44EFF6E5534}" @@ -252,6 +256,10 @@ Global {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {8AB91636-9055-4900-A72A-7CFFACDFDBF0}.Release|Any CPU.Build.0 = Release|Any CPU + {E6642255-667D-476B-B584-089AA5E6C0B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6642255-667D-476B-B584-089AA5E6C0B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6642255-667D-476B-B584-089AA5E6C0B1}.Release|Any CPU.Build.0 = Release|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -392,6 +400,10 @@ Global {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Release|Any CPU.ActiveCfg = Release|Any CPU {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6}.Release|Any CPU.Build.0 = Release|Any CPU + {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC}.Release|Any CPU.Build.0 = Release|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {19564567-1736-4626-B406-17E4E02F18B2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -450,6 +462,7 @@ Global {ADC2372B-6FE0-421D-8277-BB628E8EFC22} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {596EAFC1-0496-495C-B382-D57415FA456A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8AB91636-9055-4900-A72A-7CFFACDFDBF0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {E6642255-667D-476B-B584-089AA5E6C0B1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {A6D480BC-FDE8-4B92-A2A6-FF16BEE486AE} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {F6394475-D6F1-46E2-81BF-4BA78A40B878} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {BFDA179A-40EB-4CEB-B8E9-0DF32C65E2C5} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -485,6 +498,7 @@ Global {DA1D7ADE-452C-4369-83CC-56289176EACD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {3E55CBE8-AFE8-426D-9470-49D63CD1051C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {56D0DCA5-567F-4B3B-8B79-CB108F8EB8A6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {0F86BCE8-62E1-4BFC-AA84-63C7514C90AC} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {19564567-1736-4626-B406-17E4E02F18B2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {D53726B6-5447-47E6-B881-A44EFF6E5534} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {31EE94A0-E721-4073-B6F1-DD912D004DEF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/src/Testcontainers.PubSub/.editorconfig b/src/Testcontainers.PubSub/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.PubSub/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.PubSub/PubSubBuilder.cs b/src/Testcontainers.PubSub/PubSubBuilder.cs new file mode 100644 index 000000000..f9c14cef2 --- /dev/null +++ b/src/Testcontainers.PubSub/PubSubBuilder.cs @@ -0,0 +1,68 @@ +namespace Testcontainers.PubSub; + +/// +[PublicAPI] +public sealed class PubSubBuilder : ContainerBuilder +{ + public const string GoogleCloudCliImage = "gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators"; + + public const ushort PubSubPort = 8085; + + /// + /// Initializes a new instance of the class. + /// + public PubSubBuilder() + : this(new PubSubConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private PubSubBuilder(PubSubConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override PubSubConfiguration DockerResourceConfiguration { get; } + + /// + public override PubSubContainer Build() + { + Validate(); + return new PubSubContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override PubSubBuilder Init() + { + return base.Init() + .WithImage(GoogleCloudCliImage) + .WithPortBinding(PubSubPort, true) + .WithEntrypoint("gcloud") + .WithCommand("beta", "emulators", "pubsub", "start", "--host-port", "0.0.0.0:" + PubSubPort) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("(?s).*started.*$")); + } + + /// + protected override PubSubBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PubSubConfiguration(resourceConfiguration)); + } + + /// + protected override PubSubBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new PubSubConfiguration(resourceConfiguration)); + } + + /// + protected override PubSubBuilder Merge(PubSubConfiguration oldValue, PubSubConfiguration newValue) + { + return new PubSubBuilder(new PubSubConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.PubSub/PubSubConfiguration.cs b/src/Testcontainers.PubSub/PubSubConfiguration.cs new file mode 100644 index 000000000..f093f497b --- /dev/null +++ b/src/Testcontainers.PubSub/PubSubConfiguration.cs @@ -0,0 +1,53 @@ +namespace Testcontainers.PubSub; + +/// +[PublicAPI] +public sealed class PubSubConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public PubSubConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public PubSubConfiguration(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 PubSubConfiguration(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 PubSubConfiguration(PubSubConfiguration resourceConfiguration) + : this(new PubSubConfiguration(), 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 PubSubConfiguration(PubSubConfiguration oldValue, PubSubConfiguration newValue) + : base(oldValue, newValue) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.PubSub/PubSubContainer.cs b/src/Testcontainers.PubSub/PubSubContainer.cs new file mode 100644 index 000000000..32b07879f --- /dev/null +++ b/src/Testcontainers.PubSub/PubSubContainer.cs @@ -0,0 +1,25 @@ +namespace Testcontainers.PubSub; + +/// +[PublicAPI] +public sealed class PubSubContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public PubSubContainer(PubSubConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + /// Gets the PubSub emulator endpoint. + /// + /// The PubSub emulator endpoint. + public string GetEmulatorEndpoint() + { + return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(PubSubBuilder.PubSubPort)).ToString(); + } +} \ No newline at end of file diff --git a/src/Testcontainers.PubSub/Testcontainers.PubSub.csproj b/src/Testcontainers.PubSub/Testcontainers.PubSub.csproj new file mode 100644 index 000000000..4c05d521f --- /dev/null +++ b/src/Testcontainers.PubSub/Testcontainers.PubSub.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.PubSub/Usings.cs b/src/Testcontainers.PubSub/Usings.cs new file mode 100644 index 000000000..bf2829a65 --- /dev/null +++ b/src/Testcontainers.PubSub/Usings.cs @@ -0,0 +1,7 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.PubSub.Tests/.editorconfig b/tests/Testcontainers.PubSub.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.PubSub.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.PubSub.Tests/PubSubContainerTests.cs b/tests/Testcontainers.PubSub.Tests/PubSubContainerTests.cs new file mode 100644 index 000000000..03d0ae641 --- /dev/null +++ b/tests/Testcontainers.PubSub.Tests/PubSubContainerTests.cs @@ -0,0 +1,70 @@ +namespace Testcontainers.PubSub; + +public sealed class PubSubContainerTests : IAsyncLifetime +{ + private readonly PubSubContainer _pubSubContainer = new PubSubBuilder().Build(); + + public Task InitializeAsync() + { + return _pubSubContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _pubSubContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task SubTopicReturnsPubMessage() + { + // Given + const string helloPubSub = "Hello, PubSub!"; + + const string projectId = "hello-pub-sub"; + + const string topicId = "hello-topic"; + + const string subscriptionId = "hello-subscription"; + + var topicName = new TopicName(projectId, topicId); + + var subscriptionName = new SubscriptionName(projectId, subscriptionId); + + var message = new PubsubMessage(); + message.Data = ByteString.CopyFromUtf8(helloPubSub); + + var publisherClientBuilder = new PublisherServiceApiClientBuilder(); + publisherClientBuilder.Endpoint = _pubSubContainer.GetEmulatorEndpoint(); + publisherClientBuilder.ChannelCredentials = ChannelCredentials.Insecure; + + var subscriberClientBuilder = new SubscriberServiceApiClientBuilder(); + subscriberClientBuilder.Endpoint = _pubSubContainer.GetEmulatorEndpoint(); + subscriberClientBuilder.ChannelCredentials = ChannelCredentials.Insecure; + + // When + var publisher = await publisherClientBuilder.BuildAsync() + .ConfigureAwait(false); + + _ = await publisher.CreateTopicAsync(topicName) + .ConfigureAwait(false); + + var subscriber = await subscriberClientBuilder.BuildAsync() + .ConfigureAwait(false); + + _ = await subscriber.CreateSubscriptionAsync(subscriptionName, topicName, null, 60) + .ConfigureAwait(false); + + _ = await publisher.PublishAsync(topicName, new[] { message }) + .ConfigureAwait(false); + + var response = await subscriber.PullAsync(subscriptionName, 1) + .ConfigureAwait(false); + + await subscriber.AcknowledgeAsync(subscriptionName, response.ReceivedMessages.Select(receivedMessage => receivedMessage.AckId)) + .ConfigureAwait(false); + + // Then + Assert.Equal(helloPubSub, response.ReceivedMessages.Single().Message.Data.ToStringUtf8()); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.PubSub.Tests/Testcontainers.PubSub.Tests.csproj b/tests/Testcontainers.PubSub.Tests/Testcontainers.PubSub.Tests.csproj new file mode 100644 index 000000000..66d05c5b2 --- /dev/null +++ b/tests/Testcontainers.PubSub.Tests/Testcontainers.PubSub.Tests.csproj @@ -0,0 +1,18 @@ + + + net6.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.PubSub.Tests/Usings.cs b/tests/Testcontainers.PubSub.Tests/Usings.cs new file mode 100644 index 000000000..c6e804fe3 --- /dev/null +++ b/tests/Testcontainers.PubSub.Tests/Usings.cs @@ -0,0 +1,7 @@ +global using System.Linq; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using Google.Cloud.PubSub.V1; +global using Google.Protobuf; +global using Grpc.Core; +global using Xunit; \ No newline at end of file