diff --git a/src/Testcontainers.Kafka/KafkaBuilder.cs b/src/Testcontainers.Kafka/KafkaBuilder.cs index 99e6cbf9e..e9ec0631d 100644 --- a/src/Testcontainers.Kafka/KafkaBuilder.cs +++ b/src/Testcontainers.Kafka/KafkaBuilder.cs @@ -275,7 +275,8 @@ protected override void Validate() base.Validate(); - Predicate vendorNotFound = value => value == null && !VendorConfigurations.Any(v => v.IsImageFromVendor(DockerResourceConfiguration.Image)); + Predicate vendorNotFound = value => + value == null && !VendorConfigurations.Any(v => v.IsImageFromVendor(DockerResourceConfiguration.Image)); _ = Guard.Argument(DockerResourceConfiguration.Vendor, nameof(DockerResourceConfiguration.Vendor)) .ThrowIf(argument => vendorNotFound(argument.Value), argument => new ArgumentException(message, argument.Name)); diff --git a/src/Testcontainers.LocalStack/LocalStackBuilder.cs b/src/Testcontainers.LocalStack/LocalStackBuilder.cs index aa62d97c9..4f278d812 100644 --- a/src/Testcontainers.LocalStack/LocalStackBuilder.cs +++ b/src/Testcontainers.LocalStack/LocalStackBuilder.cs @@ -80,6 +80,20 @@ protected override LocalStackBuilder Init() request.ForPath("/_localstack/health").ForPort(LocalStackPort))); } + /// + protected override void Validate() + { + const string message = "The image '{0}' requires the LOCALSTACK_AUTH_TOKEN environment variable for LocalStack 4.15 and onwards. For more information, see https://blog.localstack.cloud/localstack-single-image-next-steps/."; + + base.Validate(); + + Predicate requiresAuthToken = value => + !value.Environments.TryGetValue("LOCALSTACK_AUTH_TOKEN", out _) && (value.Image.MatchLatestOrNightly() || value.Image.MatchVersion(v => v.Major > 4 || v.Major == 4 && v.Minor > 14)); + + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image)) + .ThrowIf(argument => requiresAuthToken(argument.Value), argument => new ArgumentException(string.Format(message, argument.Value.Image.FullName), argument.Name)); + } + /// protected override LocalStackBuilder Clone(IResourceConfiguration resourceConfiguration) { diff --git a/src/Testcontainers.LocalStack/Usings.cs b/src/Testcontainers.LocalStack/Usings.cs index 9bd35674e..cf5dd0969 100644 --- a/src/Testcontainers.LocalStack/Usings.cs +++ b/src/Testcontainers.LocalStack/Usings.cs @@ -1,6 +1,7 @@ global using System; global using System.Diagnostics.CodeAnalysis; global using Docker.DotNet.Models; +global using DotNet.Testcontainers; global using DotNet.Testcontainers.Builders; global using DotNet.Testcontainers.Configurations; global using DotNet.Testcontainers.Containers; diff --git a/src/Testcontainers.Neo4j/Neo4jBuilder.cs b/src/Testcontainers.Neo4j/Neo4jBuilder.cs index 59944600b..d926bde43 100644 --- a/src/Testcontainers.Neo4j/Neo4jBuilder.cs +++ b/src/Testcontainers.Neo4j/Neo4jBuilder.cs @@ -145,11 +145,11 @@ protected override void Validate() base.Validate(); - Predicate licenseAgreementNotAccepted = value => value.Image.Tag != null && value.Image.Tag.Contains("enterprise") - && (!value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal)); + Predicate licenseAgreementNotAccepted = value => + value.Image.Tag != null && value.Image.Tag.Contains("enterprise") && (!value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal)); _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image)) - .ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => new ArgumentException(string.Format(message, DockerResourceConfiguration.Image.FullName), argument.Name)); + .ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => new ArgumentException(string.Format(message, argument.Value.Image.FullName), argument.Name)); } /// diff --git a/src/Testcontainers.Oracle/OracleBuilder.cs b/src/Testcontainers.Oracle/OracleBuilder.cs index c92bfb6a4..6005cf4ad 100644 --- a/src/Testcontainers.Oracle/OracleBuilder.cs +++ b/src/Testcontainers.Oracle/OracleBuilder.cs @@ -139,15 +139,15 @@ protected override OracleBuilder Init() /// protected override void Validate() { - base.Validate(); - const string message = "The image '{0}' does not support configuring the database. It is only supported on Oracle 18 and onwards."; + base.Validate(); + Predicate databaseConfigurationNotSupported = value => value.Database != null && value.Image.MatchVersion(v => v.Major < 18); _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Database)) - .ThrowIf(argument => databaseConfigurationNotSupported(argument.Value), _ => new NotSupportedException(string.Format(message, DockerResourceConfiguration.Image.FullName))); + .ThrowIf(argument => databaseConfigurationNotSupported(argument.Value), argument => new NotSupportedException(string.Format(message, argument.Value.Image.FullName))); _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username)) .NotNull() diff --git a/src/Testcontainers/Builders/AbstractBuilder`4.cs b/src/Testcontainers/Builders/AbstractBuilder`4.cs index 5b7c0d857..8a826b3da 100644 --- a/src/Testcontainers/Builders/AbstractBuilder`4.cs +++ b/src/Testcontainers/Builders/AbstractBuilder`4.cs @@ -140,14 +140,14 @@ protected virtual TBuilderEntity Init() /// Thrown when a mandatory Docker resource configuration is not set. protected virtual void Validate() { - _ = Guard.Argument(DockerResourceConfiguration.Logger, nameof(IResourceConfiguration.Logger)) + _ = Guard.Argument(DockerResourceConfiguration.Logger, nameof(DockerResourceConfiguration.Logger)) .NotNull(); - _ = Guard.Argument(DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IResourceConfiguration.DockerEndpointAuthConfig)) + _ = Guard.Argument(DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(DockerResourceConfiguration.DockerEndpointAuthConfig)) .ThrowIf(argument => argument.Value == null, CreateDockerUnavailableException); const string reuseNotSupported = "Reuse cannot be used in conjunction with WithCleanUp(true)."; - _ = Guard.Argument(DockerResourceConfiguration, nameof(IResourceConfiguration.Reuse)) + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Reuse)) .ThrowIf(argument => argument.Value.Reuse.HasValue && argument.Value.Reuse.Value && !Guid.Empty.Equals(argument.Value.SessionId), argument => new ArgumentException(reuseNotSupported, argument.Name)); } diff --git a/src/Testcontainers/Builders/ContainerBuilder`3.cs b/src/Testcontainers/Builders/ContainerBuilder`3.cs index b5b8cb32a..ac05791e9 100644 --- a/src/Testcontainers/Builders/ContainerBuilder`3.cs +++ b/src/Testcontainers/Builders/ContainerBuilder`3.cs @@ -497,10 +497,10 @@ protected override void Validate() base.Validate(); const string reuseNotSupported = "Reuse cannot be used in conjunction with WithAutoRemove(true)."; - _ = Guard.Argument(DockerResourceConfiguration, nameof(IContainerConfiguration.Reuse)) + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Reuse)) .ThrowIf(argument => argument.Value.Reuse.HasValue && argument.Value.Reuse.Value && argument.Value.AutoRemove.HasValue && argument.Value.AutoRemove.Value, argument => new ArgumentException(reuseNotSupported, argument.Name)); - _ = Guard.Argument(DockerResourceConfiguration.Image, nameof(IContainerConfiguration.Image)) + _ = Guard.Argument(DockerResourceConfiguration.Image, nameof(DockerResourceConfiguration.Image)) .NotNull(); } @@ -516,7 +516,7 @@ protected virtual void ValidateLicenseAgreement() !value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal); _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image)) - .ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => new ArgumentException(string.Format(message, DockerResourceConfiguration.Image.FullName), argument.Name)); + .ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => new ArgumentException(string.Format(message, argument.Value.Image.FullName), argument.Name)); } /// diff --git a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs index 2a4f881e6..cd233c70b 100644 --- a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs +++ b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs @@ -133,7 +133,7 @@ protected override void Validate() base.Validate(); const string reuseNotSupported = "Building an image does not support the reuse feature. To keep the built image, disable the cleanup."; - _ = Guard.Argument(DockerResourceConfiguration, nameof(IImageFromDockerfileConfiguration.Reuse)) + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Reuse)) .ThrowIf(argument => argument.Value.Reuse.HasValue && argument.Value.Reuse.Value, argument => new ArgumentException(reuseNotSupported, argument.Name)); } diff --git a/src/Testcontainers/Builders/NetworkBuilder.cs b/src/Testcontainers/Builders/NetworkBuilder.cs index 43b7b957c..61a46ae11 100644 --- a/src/Testcontainers/Builders/NetworkBuilder.cs +++ b/src/Testcontainers/Builders/NetworkBuilder.cs @@ -83,7 +83,7 @@ protected override void Validate() { base.Validate(); - _ = Guard.Argument(DockerResourceConfiguration.Name, nameof(INetworkConfiguration.Name)) + _ = Guard.Argument(DockerResourceConfiguration.Name, nameof(DockerResourceConfiguration.Name)) .NotNull() .NotEmpty(); } diff --git a/src/Testcontainers/Builders/VolumeBuilder.cs b/src/Testcontainers/Builders/VolumeBuilder.cs index a56d246d1..88984eb66 100644 --- a/src/Testcontainers/Builders/VolumeBuilder.cs +++ b/src/Testcontainers/Builders/VolumeBuilder.cs @@ -68,7 +68,7 @@ protected override void Validate() { base.Validate(); - _ = Guard.Argument(DockerResourceConfiguration.Name, nameof(IVolumeConfiguration.Name)) + _ = Guard.Argument(DockerResourceConfiguration.Name, nameof(DockerResourceConfiguration.Name)) .NotNull() .NotEmpty(); } diff --git a/tests/Testcontainers.LocalStack.Tests/Dockerfile b/tests/Testcontainers.LocalStack.Tests/Dockerfile index 7fe0bd9e8..ffb418cc2 100644 --- a/tests/Testcontainers.LocalStack.Tests/Dockerfile +++ b/tests/Testcontainers.LocalStack.Tests/Dockerfile @@ -1,2 +1,2 @@ -FROM localstack/localstack:2.0.2@sha256:2b4ec60261238bfc9573193380724ed4b9ee9fa0f39726275cd8feb48a254219 +FROM localstack/localstack:4.14.0@sha256:3ebc37595918b8accb852f8048fef2aff047d465167edd655528065b07bc364a FROM localstack/localstack:1.4.0@sha256:7badf31c550f81151c485980e17542592942d7f05acc09723c5f276d41b5927d AS v1_4_0 \ No newline at end of file diff --git a/tests/Testcontainers.LocalStack.Tests/LocalStackBuilderTest.cs b/tests/Testcontainers.LocalStack.Tests/LocalStackBuilderTest.cs new file mode 100644 index 000000000..687c1a65d --- /dev/null +++ b/tests/Testcontainers.LocalStack.Tests/LocalStackBuilderTest.cs @@ -0,0 +1,38 @@ +namespace Testcontainers.LocalStack; + +public sealed class LocalStackBuilderTest +{ + [Fact] + public void LocalStack415WithoutAuthTokenThrowsArgumentException() + { + const string message = "The image 'localstack/localstack:4.15.0' requires the LOCALSTACK_AUTH_TOKEN environment variable for LocalStack 4.15 and onwards."; + ExpectArgEx(message, () => new LocalStackBuilder("localstack/localstack:4.15.0").Build()); + } + + [Fact] + public void LocalStack500WithoutAuthTokenThrowsArgumentException() + { + const string message = "The image 'localstack/localstack:5.0.0' requires the LOCALSTACK_AUTH_TOKEN environment variable for LocalStack 4.15 and onwards."; + ExpectArgEx(message, () => new LocalStackBuilder("localstack/localstack:5.0.0").Build()); + } + + [Fact] + public void LocalStack414WithoutAuthTokenDoesNotThrowArgumentException() + { + var exception = Xunit.Record.Exception(() => new LocalStackBuilder("localstack/localstack:4.14.0").Build()); + Assert.Null(exception); + } + + [Fact] + public void LocalStack415WithAuthTokenDoesNotThrowArgumentException() + { + var exception = Xunit.Record.Exception(() => new LocalStackBuilder("localstack/localstack:4.15.0").WithEnvironment("LOCALSTACK_AUTH_TOKEN", "").Build()); + Assert.Null(exception); + } + + private static void ExpectArgEx(string expectedStartString, Action testCode) + { + var exception = Assert.Throws(testCode); + Assert.StartsWith(expectedStartString, exception.Message); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs index 972a20bf0..913ec1dd9 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs @@ -39,9 +39,11 @@ internal void AuthProviderShouldBeApplicable(IDockerEndpointAuthenticationProvid [ClassData(typeof(AuthConfigTestData))] internal void AuthConfigShouldGetDockerClientEndpoint(IDockerEndpointAuthenticationConfiguration authConfig, Uri dockerClientEndpoint) { - var dockerClient = authConfig.GetDockerClientBuilder().Build(); - Assert.Equal(dockerClientEndpoint, authConfig.Endpoint); - Assert.Equal(dockerClientEndpoint, dockerClient.Options.Endpoint); + using (var dockerClient = authConfig.GetDockerClientBuilder().Build()) + { + Assert.Equal(dockerClientEndpoint, authConfig.Endpoint); + Assert.Equal(dockerClientEndpoint, dockerClient.Options.Endpoint); + } } public sealed class TestcontainersHostEndpointAuthenticationProviderTest