diff --git a/.editorconfig b/.editorconfig index d6a6d46b5..ffdbe9bc7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -455,17 +455,6 @@ dotnet_naming_rule.parameters_rule.severity = warning max_line_length = 240 -dotnet_diagnostic.SA0001.severity = none -dotnet_diagnostic.SA1600.severity = none -dotnet_diagnostic.SA1633.severity = none - -########################################## -# JetBrains Rider -########################################## - -resharper_arrange_object_creation_when_type_evident_highlighting = do_not_show -resharper_convert_to_using_declaration_highlighting = do_not_show - ########################################## # Testcontainers.Tests ########################################## diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 000000000..b8d9419b1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1 @@ +- [ ] `CHANGELOG.md` is up-to-date diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a7cfec28c..78c159959 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -16,7 +16,6 @@ env: DOTNET_CLI_TELEMETRY_OPTOUT: true DOTNET_NOLOGO: true DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_VERSION: 6.0.x TZ: CET # https://stackoverflow.com/q/53510011 jobs: @@ -37,8 +36,6 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v2 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore .NET Tools run: dotnet tool restore @@ -55,6 +52,7 @@ jobs: - name: Get Logs run: Get-ChildItem -Path . -Include *.log -Recurse | % { Get-Content -Path $_.FullName } shell: pwsh + if: always() - name: Rename Test And Coverage Results run: Get-ChildItem -Path 'test-coverage' -Filter *.xml | Rename-Item -NewName { $_.Name -Replace 'coverage', '${{ matrix.os }}'.ToLower() } @@ -109,6 +107,9 @@ jobs: Get-ChildItem -Path 'test-coverage' -Filter *.xml | % { (Get-Content $_) -Replace '[A-Za-z0-9:\-\/\\]+tests', '${{ github.workspace }}/tests' | Set-Content $_ } shell: pwsh + - name: Setup .NET + uses: actions/setup-dotnet@v2 + - name: Restore .NET Tools run: dotnet tool restore diff --git a/CHANGELOG.md b/CHANGELOG.md index 040478ad2..6bc46776b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,18 @@ ### Added +- 421 Add Azurite module (@vlaskal) +- 504 Add Elasticsearch module (@chertby) - 516 Add `ITestcontainersBuilder.WithTmpfsMount` (@chrisbbe) +- 520 Add MariaDB module (@renemadsen) +- 528 Do not require the Docker host configuration (`DockerEndpointAuthConfig`) on `TestcontainersSettings` initialization +- 538 Support optional username and password in MongoDB connection string (@the-avid-engineer) +- 540 Add Docker registry authentication provider for `DOCKER_AUTH_CONFIG` environment variable (@vova-lantsov-dev) +- 541 Allow MsSqlTestcontainerConfiguration custom database names (@enginexon) + +### Fixed + +- 525 Read ServerURL, Username and Secret field from CredsStore response to pull private Docker images ## [2.1.0] diff --git a/Directory.Build.props b/Directory.Build.props index 358386106..ea967b41d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -15,7 +15,7 @@ $(Version) $(Version) Testcontainers - Copyright (c) 2019 - 2021 Andre Hofmeister and other authors + Copyright (c) 2019 - 2022 Andre Hofmeister and other authors Andre Hofmeister and contributors Andre Hofmeister A lightweight library to run tests with throwaway instances of Docker containers. diff --git a/Packages.props b/Packages.props index 7e46ba472..3630ba010 100644 --- a/Packages.props +++ b/Packages.props @@ -15,24 +15,28 @@ - + - + + + + - - + + + - - - + + + - + diff --git a/README.md b/README.md index 79197fb5b..d3b448606 100755 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Testcontainers is a library to support tests with throwaway instances of Docker Choose from existing pre-configured configurations and start containers within a second, to support and run your tests. Or create your own containers with Dockerfiles and run your tests immediately afterward. +Get in touch with the Testcontainers team and others, and join our [Slack workspace][slack-workspace]. + ## Supported operating systems Testcontainers supports Windows, Linux, and macOS as host systems. Linux Docker containers are supported on all three operating systems. @@ -34,16 +36,18 @@ To configure a container, use the `TestcontainersBuilder + + False + OS + True + BlockScoped + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + diff --git a/build.cake b/build.cake index 18946038e..c8070bc33 100644 --- a/build.cake +++ b/build.cake @@ -1,4 +1,4 @@ -#tool nuget:?package=dotnet-sonarscanner&version=5.7.1 +#tool nuget:?package=dotnet-sonarscanner&version=5.7.2 #addin nuget:?package=Cake.Sonar&version=1.1.29 diff --git a/global.json b/global.json index edbb354d4..4d0747381 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.300", + "version": "6.0.400", "rollForward": "latestPatch" } } diff --git a/src/Testcontainers/Builders/CredsHelperProvider.cs b/src/Testcontainers/Builders/CredsHelperProvider.cs index 2edf480f7..8bdc7667b 100644 --- a/src/Testcontainers/Builders/CredsHelperProvider.cs +++ b/src/Testcontainers/Builders/CredsHelperProvider.cs @@ -90,20 +90,8 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - var username = credential.TryGetProperty("Username", out var usernameProperty) ? usernameProperty.GetString() : null; - - var password = credential.TryGetProperty("Secret", out var passwordProperty) ? passwordProperty.GetString() : null; - this.logger.DockerRegistryCredentialFound(hostname); - - if ("".Equals(username, StringComparison.OrdinalIgnoreCase)) - { - return new DockerRegistryAuthenticationConfiguration(hostname, null, null, password); - } - else - { - return new DockerRegistryAuthenticationConfiguration(hostname, username, password); - } + return new DockerRegistryAuthenticationConfiguration(hostname, credential); } } } diff --git a/src/Testcontainers/Builders/CredsStoreProvider.cs b/src/Testcontainers/Builders/CredsStoreProvider.cs index a90bac2e6..efc84465c 100644 --- a/src/Testcontainers/Builders/CredsStoreProvider.cs +++ b/src/Testcontainers/Builders/CredsStoreProvider.cs @@ -1,7 +1,6 @@ namespace DotNet.Testcontainers.Builders { using System.Text.Json; - using Docker.DotNet.Models; using DotNet.Testcontainers.Configurations; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -9,17 +8,10 @@ /// internal sealed class CredsStoreProvider : IDockerRegistryAuthenticationProvider { - private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions(); - private readonly JsonElement rootElement; private readonly ILogger logger; - static CredsStoreProvider() - { - JsonSerializerOptions.PropertyNameCaseInsensitive = true; - } - /// /// Initializes a new instance of the class. /// @@ -65,11 +57,11 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) return null; } - AuthConfig authConfig; + JsonElement credential; try { - authConfig = JsonSerializer.Deserialize(JsonDocument.Parse(credentialProviderOutput).RootElement.GetRawText(), JsonSerializerOptions); + credential = JsonDocument.Parse(credentialProviderOutput).RootElement; } catch (JsonException) { @@ -77,7 +69,7 @@ public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) } this.logger.DockerRegistryCredentialFound(hostname); - return new DockerRegistryAuthenticationConfiguration(authConfig.ServerAddress, authConfig.Username, authConfig.Password); + return new DockerRegistryAuthenticationConfiguration(hostname, credential); } } } diff --git a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs index 4c1b5d069..a1c661598 100644 --- a/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/DockerRegistryAuthenticationProvider.cs @@ -14,9 +14,11 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth { private const string DockerHub = "index.docker.io"; - private static readonly ConcurrentDictionary Credentials = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> Credentials = new ConcurrentDictionary>(); - private readonly FileInfo dockerConfigFile; + private static readonly string UserProfileDockerConfigDirectoryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker"); + + private readonly FileInfo dockerConfigFilePath; private readonly ILogger logger; @@ -26,30 +28,30 @@ internal sealed class DockerRegistryAuthenticationProvider : IDockerRegistryAuth /// The logger. [PublicAPI] public DockerRegistryAuthenticationProvider(ILogger logger) - : this(GetDefaultDockerConfigFile(), logger) + : this(GetDefaultDockerConfigFilePath(), logger) { } /// /// Initializes a new instance of the class. /// - /// The Docker config file path. + /// The Docker config file path. /// The logger. [PublicAPI] - public DockerRegistryAuthenticationProvider(string dockerConfigFile, ILogger logger) - : this(new FileInfo(dockerConfigFile), logger) + public DockerRegistryAuthenticationProvider(string dockerConfigFilePath, ILogger logger) + : this(new FileInfo(dockerConfigFilePath), logger) { } /// /// Initializes a new instance of the class. /// - /// The Docker config file path. + /// The Docker config file path. /// The logger. [PublicAPI] - public DockerRegistryAuthenticationProvider(FileInfo dockerConfigFile, ILogger logger) + public DockerRegistryAuthenticationProvider(FileInfo dockerConfigFilePath, ILogger logger) { - this.dockerConfigFile = dockerConfigFile; + this.dockerConfigFilePath = dockerConfigFilePath; this.logger = logger; } @@ -62,48 +64,61 @@ public bool IsApplicable(string hostname) /// public IDockerRegistryAuthenticationConfiguration GetAuthConfig(string hostname) { - return Credentials.GetOrAdd(hostname ?? DockerHub, this.GetUncachedAuthConfig); + var lazyAuthConfig = Credentials.GetOrAdd(hostname ?? DockerHub, key => new Lazy(() => this.GetUncachedAuthConfig(key))); + return lazyAuthConfig.Value; } - private static string GetDefaultDockerConfigFile() + private static string GetDefaultDockerConfigFilePath() { - var dockerConfigDirectory = Environment.GetEnvironmentVariable("DOCKER_CONFIG"); - return dockerConfigDirectory == null ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker", "config.json") : Path.Combine(dockerConfigDirectory, "config.json"); + var dockerConfigDirectoryPath = PropertiesFileConfiguration.Instance.GetDockerConfig() ?? EnvironmentConfiguration.Instance.GetDockerConfig() ?? UserProfileDockerConfigDirectoryPath; + return Path.Combine(dockerConfigDirectoryPath, "config.json"); } - private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string hostname) + private static JsonDocument GetDefaultDockerAuthConfig() { - IDockerRegistryAuthenticationConfiguration authConfig; + return PropertiesFileConfiguration.Instance.GetDockerAuthConfig() ?? EnvironmentConfiguration.Instance.GetDockerAuthConfig() ?? JsonDocument.Parse("{}"); + } - if (this.dockerConfigFile.Exists) + private IDockerRegistryAuthenticationConfiguration GetUncachedAuthConfig(string hostname) + { + using (var dockerAuthConfigJsonDocument = GetDefaultDockerAuthConfig()) { - using (var dockerConfigFileStream = new FileStream(this.dockerConfigFile.FullName, FileMode.Open, FileAccess.Read)) + IDockerRegistryAuthenticationConfiguration authConfig; + + if (this.dockerConfigFilePath.Exists) { - using (var dockerConfigDocument = JsonDocument.Parse(dockerConfigFileStream)) + using (var dockerConfigFileStream = new FileStream(this.dockerConfigFilePath.FullName, FileMode.Open, FileAccess.Read)) { - authConfig = new IDockerRegistryAuthenticationProvider[] - { - new CredsHelperProvider(dockerConfigDocument, this.logger), new CredsStoreProvider(dockerConfigDocument, this.logger), new Base64Provider(dockerConfigDocument, this.logger), - } - .AsParallel() - .Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname)) - .FirstOrDefault(authenticationProvider => authenticationProvider != null); + using (var dockerConfigJsonDocument = JsonDocument.Parse(dockerConfigFileStream)) + { + authConfig = new IDockerRegistryAuthenticationProvider[] + { + new CredsHelperProvider(dockerConfigJsonDocument, this.logger), + new CredsStoreProvider(dockerConfigJsonDocument, this.logger), + new Base64Provider(dockerConfigJsonDocument, this.logger), + new Base64Provider(dockerAuthConfigJsonDocument, this.logger), + } + .AsParallel() + .Select(authenticationProvider => authenticationProvider.GetAuthConfig(hostname)) + .FirstOrDefault(authenticationProvider => authenticationProvider != null); + } } } - } - else - { - this.logger.DockerConfigFileNotFound(this.dockerConfigFile.FullName); - return default(DockerRegistryAuthenticationConfiguration); - } + else + { + this.logger.DockerConfigFileNotFound(this.dockerConfigFilePath.FullName); + IDockerRegistryAuthenticationProvider authConfigProvider = new Base64Provider(dockerAuthConfigJsonDocument, this.logger); + authConfig = authConfigProvider.GetAuthConfig(hostname); + } + + if (authConfig != null) + { + return authConfig; + } - if (authConfig == null) - { this.logger.DockerRegistryCredentialNotFound(hostname); return default(DockerRegistryAuthenticationConfiguration); } - - return authConfig; } } } diff --git a/src/Testcontainers/Builders/EnvironmentEndpointAuthenticationProvider.cs b/src/Testcontainers/Builders/EnvironmentEndpointAuthenticationProvider.cs index d9cb1f5cc..f725eff0b 100644 --- a/src/Testcontainers/Builders/EnvironmentEndpointAuthenticationProvider.cs +++ b/src/Testcontainers/Builders/EnvironmentEndpointAuthenticationProvider.cs @@ -8,11 +8,12 @@ internal sealed class EnvironmentEndpointAuthenticationProvider : DockerEndpoint { private readonly Uri dockerEngine; + /// + /// Initializes a new instance of the class. + /// public EnvironmentEndpointAuthenticationProvider() { - ICustomConfiguration propertiesFileConfiguration = new PropertiesFileConfiguration(); - ICustomConfiguration environmentConfiguration = new EnvironmentConfiguration(); - this.dockerEngine = propertiesFileConfiguration.GetDockerHost() ?? environmentConfiguration.GetDockerHost(); + this.dockerEngine = PropertiesFileConfiguration.Instance.GetDockerHost() ?? EnvironmentConfiguration.Instance.GetDockerHost(); } /// diff --git a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs index 58eba43ab..e0183d6b9 100644 --- a/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs +++ b/src/Testcontainers/Builders/ImageFromDockerfileBuilder.cs @@ -75,7 +75,10 @@ public IImageFromDockerfileBuilder WithBuildArgument(string name, string value) /// public Task Build() { - ITestcontainersClient client = new TestcontainersClient(this.DockerResourceConfiguration.DockerEndpointAuthConfig, TestcontainersSettings.Logger); + _ = Guard.Argument(this.DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IDockerResourceConfiguration.DockerEndpointAuthConfig)) + .DockerEndpointAuthConfigIsSet(); + + ITestcontainersClient client = new TestcontainersClient(this.DockerResourceConfiguration.SessionId, this.DockerResourceConfiguration.DockerEndpointAuthConfig, TestcontainersSettings.Logger); return client.BuildAsync(this.DockerResourceConfiguration); } diff --git a/src/Testcontainers/Builders/TestcontainersBuilder.cs b/src/Testcontainers/Builders/TestcontainersBuilder.cs index f2d48a871..24f4db0e8 100644 --- a/src/Testcontainers/Builders/TestcontainersBuilder.cs +++ b/src/Testcontainers/Builders/TestcontainersBuilder.cs @@ -309,7 +309,10 @@ public ITestcontainersBuilder WithStartupCallback(Func public TDockerContainer Build() { - Guard.Argument(this.DockerResourceConfiguration.Image, nameof(ITestcontainersConfiguration.Image)) + _ = Guard.Argument(this.DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IDockerResourceConfiguration.DockerEndpointAuthConfig)) + .DockerEndpointAuthConfigIsSet(); + + _ = Guard.Argument(this.DockerResourceConfiguration.Image, nameof(ITestcontainersConfiguration.Image)) .NotNull(); #pragma warning disable S3011 diff --git a/src/Testcontainers/Builders/TestcontainersBuilderAzuriteExtension.cs b/src/Testcontainers/Builders/TestcontainersBuilderAzuriteExtension.cs new file mode 100644 index 000000000..3cf3ff694 --- /dev/null +++ b/src/Testcontainers/Builders/TestcontainersBuilderAzuriteExtension.cs @@ -0,0 +1,155 @@ +namespace DotNet.Testcontainers.Builders +{ + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + + /// + /// This class applies the extended Testcontainer configurations for Azurite. + /// + [PublicAPI] + public static class TestcontainersBuilderAzuriteExtension + { + public static ITestcontainersBuilder WithAzurite(this ITestcontainersBuilder builder, AzuriteTestcontainerConfiguration configuration) + { + var blobServiceEnabled = configuration.AllServicesEnabled || configuration.BlobServiceOnlyEnabled; + var queueServiceEnabled = configuration.AllServicesEnabled || configuration.QueueServiceOnlyEnabled; + var tableServiceEnabled = configuration.AllServicesEnabled || configuration.TableServiceOnlyEnabled; + + builder = builder + .WithImage(configuration.Image) + .WithWaitStrategy(configuration.WaitStrategy) + .ConfigureContainer(container => + { + container.ContainerBlobPort = blobServiceEnabled ? configuration.BlobContainerPort : 0; + container.ContainerQueuePort = queueServiceEnabled ? configuration.QueueContainerPort : 0; + container.ContainerTablePort = tableServiceEnabled ? configuration.TableContainerPort : 0; + }); + + if (blobServiceEnabled) + { + builder = builder + .WithExposedPort(configuration.BlobContainerPort) + .WithPortBinding(configuration.BlobPort, configuration.BlobContainerPort); + } + + if (queueServiceEnabled) + { + builder = builder + .WithExposedPort(configuration.QueueContainerPort) + .WithPortBinding(configuration.QueuePort, configuration.QueueContainerPort); + } + + if (tableServiceEnabled) + { + builder = builder + .WithExposedPort(configuration.TableContainerPort) + .WithPortBinding(configuration.TablePort, configuration.TableContainerPort); + } + + if (configuration.Location != null) + { + builder = builder + .WithBindMount(configuration.Location, AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath); + } + + return builder + .WithCommand(GetExecutable(configuration)) + .WithCommand(GetEnabledServices(configuration)) + .WithCommand(GetWorkspaceDirectoryPath()) + .WithCommand(GetDebugModeEnabled(configuration)) + .WithCommand(GetSilentModeEnabled(configuration)) + .WithCommand(GetLooseModeEnabled(configuration)) + .WithCommand(GetSkipApiVersionCheckEnabled(configuration)) + .WithCommand(GetProductStyleUrlDisabled(configuration)); + } + + private static string GetExecutable(AzuriteTestcontainerConfiguration configuration) + { + if (configuration.BlobServiceOnlyEnabled) + { + return "azurite-blob"; + } + + if (configuration.QueueServiceOnlyEnabled) + { + return "azurite-queue"; + } + + if (configuration.TableServiceOnlyEnabled) + { + return "azurite-table"; + } + + return "azurite"; + } + + private static string[] GetEnabledServices(AzuriteTestcontainerConfiguration configuration) + { + const string defaultRemoteEndpoint = "0.0.0.0"; + + IList args = new List(); + + if (configuration.AllServicesEnabled || configuration.BlobServiceOnlyEnabled) + { + args.Add("--blobHost"); + args.Add(defaultRemoteEndpoint); + args.Add("--blobPort"); + args.Add(configuration.BlobContainerPort.ToString(CultureInfo.InvariantCulture)); + } + + if (configuration.AllServicesEnabled || configuration.QueueServiceOnlyEnabled) + { + args.Add("--queueHost"); + args.Add(defaultRemoteEndpoint); + args.Add("--queuePort"); + args.Add(configuration.QueueContainerPort.ToString(CultureInfo.InvariantCulture)); + } + + if (configuration.AllServicesEnabled || configuration.TableServiceOnlyEnabled) + { + args.Add("--tableHost"); + args.Add(defaultRemoteEndpoint); + args.Add("--tablePort"); + args.Add(configuration.TableContainerPort.ToString(CultureInfo.InvariantCulture)); + } + + return args.ToArray(); + } + + private static string[] GetWorkspaceDirectoryPath() + { + return new[] { "--location", AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath }; + } + + private static string[] GetDebugModeEnabled(AzuriteTestcontainerConfiguration configuration) + { + var debugLogFilePath = Path.Combine(AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath, "debug.log"); + return configuration.DebugModeEnabled ? new[] { "--debug", debugLogFilePath } : null; + } + + private static string GetSilentModeEnabled(AzuriteTestcontainerConfiguration configuration) + { + return configuration.SilentModeEnabled ? "--silent" : null; + } + + private static string GetLooseModeEnabled(AzuriteTestcontainerConfiguration configuration) + { + return configuration.LooseModeEnabled ? "--loose" : null; + } + + private static string GetSkipApiVersionCheckEnabled(AzuriteTestcontainerConfiguration configuration) + { + return configuration.SkipApiVersionCheckEnabled ? "--skipApiVersionCheck" : null; + } + + private static string GetProductStyleUrlDisabled(AzuriteTestcontainerConfiguration configuration) + { + return configuration.ProductStyleUrlDisabled ? "--disableProductStyleUrl" : null; + } + } +} diff --git a/src/Testcontainers/Builders/TestcontainersNetworkBuilder.cs b/src/Testcontainers/Builders/TestcontainersNetworkBuilder.cs index c233b68ee..1620b9478 100644 --- a/src/Testcontainers/Builders/TestcontainersNetworkBuilder.cs +++ b/src/Testcontainers/Builders/TestcontainersNetworkBuilder.cs @@ -43,6 +43,9 @@ public ITestcontainersNetworkBuilder WithDriver(NetworkDriver driver) /// public IDockerNetwork Build() { + _ = Guard.Argument(this.DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IDockerResourceConfiguration.DockerEndpointAuthConfig)) + .DockerEndpointAuthConfigIsSet(); + return new NonExistingDockerNetwork(this.DockerResourceConfiguration, TestcontainersSettings.Logger); } diff --git a/src/Testcontainers/Builders/TestcontainersVolumeBuilder.cs b/src/Testcontainers/Builders/TestcontainersVolumeBuilder.cs index a186849a3..33a74c03a 100644 --- a/src/Testcontainers/Builders/TestcontainersVolumeBuilder.cs +++ b/src/Testcontainers/Builders/TestcontainersVolumeBuilder.cs @@ -37,6 +37,9 @@ public ITestcontainersVolumeBuilder WithName(string name) /// public IDockerVolume Build() { + _ = Guard.Argument(this.DockerResourceConfiguration.DockerEndpointAuthConfig, nameof(IDockerResourceConfiguration.DockerEndpointAuthConfig)) + .DockerEndpointAuthConfigIsSet(); + return new NonExistingDockerVolume(this.DockerResourceConfiguration, TestcontainersSettings.Logger); } diff --git a/src/Testcontainers/Clients/DefaultLabels.cs b/src/Testcontainers/Clients/DefaultLabels.cs index e19f3e581..db108eb09 100644 --- a/src/Testcontainers/Clients/DefaultLabels.cs +++ b/src/Testcontainers/Clients/DefaultLabels.cs @@ -8,6 +8,10 @@ namespace DotNet.Testcontainers.Clients internal sealed class DefaultLabels : ReadOnlyDictionary { + static DefaultLabels() + { + } + private DefaultLabels(Guid resourceReaperSessionId) : base(new Dictionary { diff --git a/src/Testcontainers/Clients/DockerApiClient.cs b/src/Testcontainers/Clients/DockerApiClient.cs index 33332ad69..0e669e546 100644 --- a/src/Testcontainers/Clients/DockerApiClient.cs +++ b/src/Testcontainers/Clients/DockerApiClient.cs @@ -7,17 +7,22 @@ namespace DotNet.Testcontainers.Clients internal abstract class DockerApiClient { - private static readonly ConcurrentDictionary Clients = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary> Clients = new ConcurrentDictionary>(); - protected DockerApiClient(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig) + protected DockerApiClient(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig) { - this.Docker = Clients.GetOrAdd(dockerEndpointAuthConfig.Endpoint, _ => - { - using (var dockerClientConfiguration = dockerEndpointAuthConfig.GetDockerClientConfiguration()) + _ = sessionId; + + var lazyDockerClient = Clients.GetOrAdd(dockerEndpointAuthConfig.Endpoint, _ => + new Lazy(() => { - return dockerClientConfiguration.CreateClient(); - } - }); + using (var dockerClientConfiguration = dockerEndpointAuthConfig.GetDockerClientConfiguration()) + { + return dockerClientConfiguration.CreateClient(); + } + })); + + this.Docker = lazyDockerClient.Value; } protected IDockerClient Docker { get; } diff --git a/src/Testcontainers/Clients/DockerContainerOperations.cs b/src/Testcontainers/Clients/DockerContainerOperations.cs index 59c82b0e8..ea130eb9b 100644 --- a/src/Testcontainers/Clients/DockerContainerOperations.cs +++ b/src/Testcontainers/Clients/DockerContainerOperations.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Clients { + using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -14,8 +15,8 @@ internal sealed class DockerContainerOperations : DockerApiClient, IDockerContai { private readonly ILogger logger; - public DockerContainerOperations(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) - : base(dockerEndpointAuthConfig) + public DockerContainerOperations(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + : base(sessionId, dockerEndpointAuthConfig) { this.logger = logger; } diff --git a/src/Testcontainers/Clients/DockerImageOperations.cs b/src/Testcontainers/Clients/DockerImageOperations.cs index d376f183e..18ca082a6 100644 --- a/src/Testcontainers/Clients/DockerImageOperations.cs +++ b/src/Testcontainers/Clients/DockerImageOperations.cs @@ -17,8 +17,8 @@ internal sealed class DockerImageOperations : DockerApiClient, IDockerImageOpera private readonly TraceProgress traceProgress; - public DockerImageOperations(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) - : base(dockerEndpointAuthConfig) + public DockerImageOperations(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + : base(sessionId, dockerEndpointAuthConfig) { this.logger = logger; this.traceProgress = new TraceProgress(logger); diff --git a/src/Testcontainers/Clients/DockerNetworkOperations.cs b/src/Testcontainers/Clients/DockerNetworkOperations.cs index 810baa793..f1c0b1a78 100644 --- a/src/Testcontainers/Clients/DockerNetworkOperations.cs +++ b/src/Testcontainers/Clients/DockerNetworkOperations.cs @@ -13,8 +13,8 @@ internal sealed class DockerNetworkOperations : DockerApiClient, IDockerNetworkO { private readonly ILogger logger; - public DockerNetworkOperations(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) - : base(dockerEndpointAuthConfig) + public DockerNetworkOperations(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + : base(sessionId, dockerEndpointAuthConfig) { this.logger = logger; } diff --git a/src/Testcontainers/Clients/DockerSystemOperations.cs b/src/Testcontainers/Clients/DockerSystemOperations.cs index a3d7affc5..77fabcfaa 100644 --- a/src/Testcontainers/Clients/DockerSystemOperations.cs +++ b/src/Testcontainers/Clients/DockerSystemOperations.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Clients { + using System; using System.Threading; using System.Threading.Tasks; using DotNet.Testcontainers.Configurations; @@ -7,8 +8,8 @@ namespace DotNet.Testcontainers.Clients internal sealed class DockerSystemOperations : DockerApiClient, IDockerSystemOperations { - public DockerSystemOperations(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) - : base(dockerEndpointAuthConfig) + public DockerSystemOperations(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + : base(sessionId, dockerEndpointAuthConfig) { _ = logger; } diff --git a/src/Testcontainers/Clients/DockerVolumeOperations.cs b/src/Testcontainers/Clients/DockerVolumeOperations.cs index d1870a29b..c05dbc281 100644 --- a/src/Testcontainers/Clients/DockerVolumeOperations.cs +++ b/src/Testcontainers/Clients/DockerVolumeOperations.cs @@ -13,8 +13,8 @@ internal sealed class DockerVolumeOperations : DockerApiClient, IDockerVolumeOpe { private readonly ILogger logger; - public DockerVolumeOperations(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) - : base(dockerEndpointAuthConfig) + public DockerVolumeOperations(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + : base(sessionId, dockerEndpointAuthConfig) { this.logger = logger; } diff --git a/src/Testcontainers/Clients/TestcontainersClient.cs b/src/Testcontainers/Clients/TestcontainersClient.cs index 66c23eb58..2e38a23d0 100644 --- a/src/Testcontainers/Clients/TestcontainersClient.cs +++ b/src/Testcontainers/Clients/TestcontainersClient.cs @@ -34,6 +34,7 @@ internal sealed class TestcontainersClient : ITestcontainersClient /// public TestcontainersClient() : this( + Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger) { @@ -42,13 +43,14 @@ public TestcontainersClient() /// /// Initializes a new instance of the class. /// + /// The session id. /// The Docker endpoint authentication configuration. /// The logger. - public TestcontainersClient(IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) + public TestcontainersClient(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, ILogger logger) : this( - new DockerContainerOperations(dockerEndpointAuthConfig, logger), - new DockerImageOperations(dockerEndpointAuthConfig, logger), - new DockerSystemOperations(dockerEndpointAuthConfig, logger), + new DockerContainerOperations(sessionId, dockerEndpointAuthConfig, logger), + new DockerImageOperations(sessionId, dockerEndpointAuthConfig, logger), + new DockerSystemOperations(sessionId, dockerEndpointAuthConfig, logger), new DockerRegistryAuthenticationProvider(logger)) { } @@ -233,7 +235,7 @@ public async Task RunAsync(ITestcontainersConfiguration configuration, C var isWindowsEngineEnabled = await this.GetIsWindowsEngineEnabled(ct) .ConfigureAwait(false); - if (!isWindowsEngineEnabled && ResourceReaper.DefaultSessionId.ToString("D").Equals(configuration.Labels[ResourceReaper.ResourceReaperSessionLabel], StringComparison.OrdinalIgnoreCase)) + if (!isWindowsEngineEnabled && ResourceReaper.DefaultSessionId.Equals(configuration.SessionId)) { _ = await ResourceReaper.GetAndStartDefaultAsync(configuration.DockerEndpointAuthConfig, ct) .ConfigureAwait(false); diff --git a/src/Testcontainers/Configurations/CustomConfiguration.cs b/src/Testcontainers/Configurations/CustomConfiguration.cs index 38a8a99a6..8c722a918 100644 --- a/src/Testcontainers/Configurations/CustomConfiguration.cs +++ b/src/Testcontainers/Configurations/CustomConfiguration.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Text.Json; using DotNet.Testcontainers.Images; internal abstract class CustomConfiguration @@ -13,11 +14,36 @@ protected CustomConfiguration(IReadOnlyDictionary properties) this.properties = properties; } + protected string GetDockerConfig(string propertyName) + { + _ = this.properties.TryGetValue(propertyName, out var propertyValue); + return propertyValue; + } + protected Uri GetDockerHost(string propertyName) { return this.properties.TryGetValue(propertyName, out var propertyValue) && Uri.TryCreate(propertyValue, UriKind.RelativeOrAbsolute, out var dockerHost) ? dockerHost : null; } + protected JsonDocument GetDockerAuthConfig(string propertyName) + { + _ = this.properties.TryGetValue(propertyName, out var propertyValue); + + if (string.IsNullOrEmpty(propertyValue)) + { + return null; + } + + try + { + return JsonDocument.Parse(propertyValue); + } + catch (Exception) + { + return null; + } + } + protected bool GetRyukDisabled(string propertyName) { return this.properties.TryGetValue(propertyName, out var propertyValue) && bool.TryParse(propertyValue, out var ryukDisabled) && ryukDisabled; diff --git a/src/Testcontainers/Configurations/DockerResourceConfiguration.cs b/src/Testcontainers/Configurations/DockerResourceConfiguration.cs index 4980fb40c..5f21b7525 100644 --- a/src/Testcontainers/Configurations/DockerResourceConfiguration.cs +++ b/src/Testcontainers/Configurations/DockerResourceConfiguration.cs @@ -1,9 +1,11 @@ namespace DotNet.Testcontainers.Configurations { + using System; using System.Collections.Generic; + using DotNet.Testcontainers.Containers; /// - public class DockerResourceConfiguration : IDockerResourceConfiguration + internal class DockerResourceConfiguration : IDockerResourceConfiguration { /// /// Initializes a new instance of the class. @@ -25,8 +27,12 @@ public DockerResourceConfiguration( { this.DockerEndpointAuthConfig = dockerEndpointAuthenticationConfiguration; this.Labels = labels; + this.SessionId = labels != null && labels.TryGetValue(ResourceReaper.ResourceReaperSessionLabel, out var resourceReaperSessionId) && Guid.TryParseExact(resourceReaperSessionId, "D", out var sessionId) ? sessionId : Guid.Empty; } + /// + public Guid SessionId { get; } + /// public IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig { get; } diff --git a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs index 52522f765..d99b55926 100644 --- a/src/Testcontainers/Configurations/EnvironmentConfiguration.cs +++ b/src/Testcontainers/Configurations/EnvironmentConfiguration.cs @@ -2,6 +2,7 @@ { using System; using System.Linq; + using System.Text.Json; using DotNet.Testcontainers.Images; /// @@ -13,27 +14,51 @@ internal sealed class EnvironmentConfiguration : CustomConfiguration, ICustomCon private const string DockerHost = "DOCKER_HOST"; + private const string DockerAuthConfig = "DOCKER_AUTH_CONFIG"; + private const string RyukDisabled = "TESTCONTAINERS_RYUK_DISABLED"; private const string RyukContainerImage = "TESTCONTAINERS_RYUK_CONTAINER_IMAGE"; private const string HubImageNamePrefix = "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX"; + static EnvironmentConfiguration() + { + } + /// /// Initializes a new instance of the class. /// public EnvironmentConfiguration() - : base(new[] { DockerConfig, DockerHost, RyukDisabled, RyukContainerImage, HubImageNamePrefix } + : base(new[] { DockerConfig, DockerHost, DockerAuthConfig, RyukDisabled, RyukContainerImage, HubImageNamePrefix } .ToDictionary(key => key, Environment.GetEnvironmentVariable)) { } + /// + /// Gets the instance. + /// + public static ICustomConfiguration Instance { get; } + = new EnvironmentConfiguration(); + + /// + public string GetDockerConfig() + { + return this.GetDockerConfig(DockerConfig); + } + /// public Uri GetDockerHost() { return this.GetDockerHost(DockerHost); } + /// + public JsonDocument GetDockerAuthConfig() + { + return this.GetDockerAuthConfig(DockerAuthConfig); + } + /// public bool GetRyukDisabled() { diff --git a/src/Testcontainers/Configurations/ICustomConfiguration.cs b/src/Testcontainers/Configurations/ICustomConfiguration.cs index f39a7d318..9256cb884 100644 --- a/src/Testcontainers/Configurations/ICustomConfiguration.cs +++ b/src/Testcontainers/Configurations/ICustomConfiguration.cs @@ -1,6 +1,7 @@ namespace DotNet.Testcontainers.Configurations { using System; + using System.Text.Json; using DotNet.Testcontainers.Images; using JetBrains.Annotations; @@ -9,6 +10,14 @@ /// internal interface ICustomConfiguration { + /// + /// Gets the Docker config custom configuration. + /// + /// The Docker config custom configuration. + /// https://www.testcontainers.org/features/configuration/#customizing-docker-host-detection. + [CanBeNull] + string GetDockerConfig(); + /// /// Gets the Docker host custom configuration. /// @@ -17,6 +26,14 @@ internal interface ICustomConfiguration [CanBeNull] Uri GetDockerHost(); + /// + /// Gets the Docker registry authentication custom configuration. + /// + /// The Docker authentication custom configuration. + /// https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#access-an-image-from-a-private-container-registry. + [CanBeNull] + JsonDocument GetDockerAuthConfig(); + /// /// Gets the Ryuk disabled custom configuration. /// diff --git a/src/Testcontainers/Configurations/IDockerResourceConfiguration.cs b/src/Testcontainers/Configurations/IDockerResourceConfiguration.cs index 7c1618c5d..42d4afc36 100644 --- a/src/Testcontainers/Configurations/IDockerResourceConfiguration.cs +++ b/src/Testcontainers/Configurations/IDockerResourceConfiguration.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Configurations { + using System; using System.Collections.Generic; /// @@ -7,6 +8,11 @@ /// public interface IDockerResourceConfiguration { + /// + /// Gets the session id. + /// + Guid SessionId { get; } + /// /// Gets the Docker endpoint authentication configuration. /// diff --git a/src/Testcontainers/Configurations/Images/DockerRegistryAuthenticationConfiguration.cs b/src/Testcontainers/Configurations/Images/DockerRegistryAuthenticationConfiguration.cs index 71da468c4..3bae3c554 100644 --- a/src/Testcontainers/Configurations/Images/DockerRegistryAuthenticationConfiguration.cs +++ b/src/Testcontainers/Configurations/Images/DockerRegistryAuthenticationConfiguration.cs @@ -1,5 +1,8 @@ namespace DotNet.Testcontainers.Configurations { + using System; + using System.Text.Json; + /// internal readonly struct DockerRegistryAuthenticationConfiguration : IDockerRegistryAuthenticationConfiguration { @@ -22,6 +25,35 @@ public DockerRegistryAuthenticationConfiguration( this.IdentityToken = identityToken; } + /// + /// Initializes a new instance of the struct. + /// + /// The Docker registry endpoint. + /// The CredHelpers or CredsStore JSON response. + public DockerRegistryAuthenticationConfiguration( + string registryEndpoint, + JsonElement credential) + { + var username = credential.TryGetProperty(nameof(this.Username), out var usernameProperty) ? usernameProperty.GetString() : null; + + var password = credential.TryGetProperty("Secret", out var passwordProperty) ? passwordProperty.GetString() : null; + + if ("".Equals(username, StringComparison.OrdinalIgnoreCase)) + { + this.RegistryEndpoint = registryEndpoint; + this.Username = null; + this.Password = null; + this.IdentityToken = password; + } + else + { + this.RegistryEndpoint = registryEndpoint; + this.Username = username; + this.Password = password; + this.IdentityToken = null; + } + } + /// public string RegistryEndpoint { get; } diff --git a/src/Testcontainers/Configurations/Modules/Databases/AzuriteTestcontainerConfiguration.cs b/src/Testcontainers/Configurations/Modules/Databases/AzuriteTestcontainerConfiguration.cs new file mode 100644 index 000000000..174359b5f --- /dev/null +++ b/src/Testcontainers/Configurations/Modules/Databases/AzuriteTestcontainerConfiguration.cs @@ -0,0 +1,275 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System; + using DotNet.Testcontainers.Builders; + using JetBrains.Annotations; + + /// + /// This class represents an extended Testcontainer configuration for Azurite. + /// + [PublicAPI] + public sealed class AzuriteTestcontainerConfiguration + { + /// + /// Default workspace directory path '/data/'. + /// + [PublicAPI] + public const string DefaultWorkspaceDirectoryPath = "/data/"; + + private const string AzuriteImage = "mcr.microsoft.com/azure-storage/azurite:3.18.0"; + + private const int DefaultBlobPort = 10000; + + private const int DefaultQueuePort = 10001; + + private const int DefaultTablePort = 10002; + + private AzuriteServices enabledServices = AzuriteServices.All; + + /// + /// Initializes a new instance of the class. + /// + public AzuriteTestcontainerConfiguration() + : this(AzuriteImage) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker image. + public AzuriteTestcontainerConfiguration(string image) + { + this.Image = image; + } + + /// + /// Azurite services. + /// + [Flags] + internal enum AzuriteServices + { + /// + /// The blob service. + /// + Blob = 1, + + /// + /// The queue service. + /// + Queue = 2, + + /// + /// The table service. + /// + Table = 4, + + /// + /// All services. + /// + All = Blob | Queue | Table, + } + + /// + /// Gets the Docker image. + /// + [PublicAPI] + public string Image { get; } + + /// + /// Gets the wait strategy. + /// + /// + /// Uses as default value. + /// + [PublicAPI] + public IWaitForContainerOS WaitStrategy + { + get + { + var waitStrategy = Wait.ForUnixContainer(); + waitStrategy = this.enabledServices.HasFlag(AzuriteServices.Blob) ? waitStrategy.UntilPortIsAvailable(this.BlobContainerPort) : waitStrategy; + waitStrategy = this.enabledServices.HasFlag(AzuriteServices.Queue) ? waitStrategy.UntilPortIsAvailable(this.QueueContainerPort) : waitStrategy; + waitStrategy = this.enabledServices.HasFlag(AzuriteServices.Table) ? waitStrategy.UntilPortIsAvailable(this.TableContainerPort) : waitStrategy; + return waitStrategy; + } + } + + /// + /// Gets or sets the host blob port. + /// + /// + /// Bound to the container blob port. + /// + [PublicAPI] + public int BlobPort { get; set; } + + /// + /// Gets or sets the container blob port. + /// + [PublicAPI] + public int BlobContainerPort { get; set; } + = DefaultBlobPort; + + /// + /// Gets or sets a value indicating whether the blob service runs standalone or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool BlobServiceOnlyEnabled + { + get + { + return AzuriteServices.Blob.Equals(this.enabledServices); + } + + set + { + this.enabledServices = value ? AzuriteServices.Blob : AzuriteServices.All; + } + } + + /// + /// Gets or sets the host queue port. + /// + /// + /// Bound to the container queue port. + /// + [PublicAPI] + public int QueuePort { get; set; } + + /// + /// Gets or sets the container queue port. + /// + [PublicAPI] + public int QueueContainerPort { get; set; } + = DefaultQueuePort; + + /// + /// Gets or sets a value indicating whether the queue service runs standalone or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool QueueServiceOnlyEnabled + { + get + { + return AzuriteServices.Queue.Equals(this.enabledServices); + } + + set + { + this.enabledServices = value ? AzuriteServices.Queue : AzuriteServices.All; + } + } + + /// + /// Gets or sets the host table port. + /// + /// + /// Bound to the container queue port. + /// + [PublicAPI] + public int TablePort { get; set; } + + /// + /// Gets or sets the container table port. + /// + [PublicAPI] + public int TableContainerPort { get; set; } + = DefaultTablePort; + + /// + /// Gets or sets a value indicating whether the table service runs standalone or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool TableServiceOnlyEnabled + { + get + { + return AzuriteServices.Table.Equals(this.enabledServices); + } + + set + { + this.enabledServices = value ? AzuriteServices.Table : AzuriteServices.All; + } + } + + /// + /// Gets a value indicating whether all Azurite service are enabled or not. + /// + [PublicAPI] + public bool AllServicesEnabled + { + get + { + return AzuriteServices.All.Equals(this.enabledServices); + } + } + + /// + /// Gets or sets the workspace directory path. + /// + /// + /// Corresponds to the default workspace directory path. + /// + [PublicAPI] + [CanBeNull] + public string Location { get; set; } + + /// + /// Gets or sets a value indicating whether debug mode is enabled or not. + /// + /// + /// Writes logs to the workspace directory path. + /// Default value is false. + /// + [PublicAPI] + public bool DebugModeEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether silent mode is enabled or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool SilentModeEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether loose mode is enabled or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool LooseModeEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether skip API version check is enabled or not. + /// + /// + /// Default value is false. + /// + [PublicAPI] + public bool SkipApiVersionCheckEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether product style URL is enabled or not. + /// + /// + /// Parses storage account name from the URI path, instead of the URI host. + /// Default value is false. + /// + [PublicAPI] + public bool ProductStyleUrlDisabled { get; set; } + } +} diff --git a/src/Testcontainers/Configurations/Modules/Databases/ElasticsearchTestcontainerConfiguration.cs b/src/Testcontainers/Configurations/Modules/Databases/ElasticsearchTestcontainerConfiguration.cs new file mode 100644 index 000000000..926d37be1 --- /dev/null +++ b/src/Testcontainers/Configurations/Modules/Databases/ElasticsearchTestcontainerConfiguration.cs @@ -0,0 +1,57 @@ +namespace DotNet.Testcontainers.Configurations +{ + using System; + using DotNet.Testcontainers.Builders; + using JetBrains.Annotations; + + /// + [PublicAPI] + public class ElasticsearchTestcontainerConfiguration : TestcontainerDatabaseConfiguration + { + private const string ElasticsearchImage = "elasticsearch:8.3.2"; + + private const int ElasticsearchPort = 9200; + + /// + /// Initializes a new instance of the class. + /// + public ElasticsearchTestcontainerConfiguration() + : this(ElasticsearchImage) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker image. + public ElasticsearchTestcontainerConfiguration(string image) + : base(image, ElasticsearchPort) + { + } + + /// + public override string Database + { + get => string.Empty; + set => throw new NotImplementedException(); + } + + /// + public override string Username + { + get => "elastic"; + set => throw new NotImplementedException(); + } + + /// + public override string Password + { + get => this.Environments["ELASTIC_PASSWORD"]; + set => this.Environments["ELASTIC_PASSWORD"] = value; + } + + /// + public override IWaitForContainerOS WaitStrategy => Wait.ForUnixContainer() + .UntilPortIsAvailable(this.DefaultPort); + } +} diff --git a/src/Testcontainers/Configurations/Modules/Databases/MariaDbTestcontainerConfiguration.cs b/src/Testcontainers/Configurations/Modules/Databases/MariaDbTestcontainerConfiguration.cs new file mode 100644 index 000000000..1a3d9eef7 --- /dev/null +++ b/src/Testcontainers/Configurations/Modules/Databases/MariaDbTestcontainerConfiguration.cs @@ -0,0 +1,66 @@ +namespace DotNet.Testcontainers.Configurations +{ + using DotNet.Testcontainers.Builders; + using JetBrains.Annotations; + + /// + [PublicAPI] + public class MariaDbTestcontainerConfiguration : TestcontainerDatabaseConfiguration + { + private const string MariaDbImage = "mariadb:10.8"; + + private const int MariaDbPort = 3306; + + /// + /// Initializes a new instance of the class. + /// + public MariaDbTestcontainerConfiguration() + : this(MariaDbImage) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker image. + public MariaDbTestcontainerConfiguration(string image) + : base(image, MariaDbPort) + { + this.Environments["MYSQL_ALLOW_EMPTY_PASSWORD"] = "yes"; + } + + /// + public override string Database + { + get => this.Environments["MYSQL_DATABASE"]; + set => this.Environments["MYSQL_DATABASE"] = value; + } + + /// + public override string Username + { + get => this.Environments["MYSQL_USER"]; + set => this.Environments["MYSQL_USER"] = value; + } + + /// + public override string Password + { + get => this.Environments["MYSQL_PASSWORD"]; + set => this.Environments["MYSQL_PASSWORD"] = value; + } + + /// + /// Gets or sets the MariaDB root superuser account password. + /// + public string RootPassword + { + get => this.Environments["MARIADB_ROOT_PASSWORD"]; + set => this.Environments["MARIADB_ROOT_PASSWORD"] = value; + } + + /// + public override IWaitForContainerOS WaitStrategy => Wait.ForUnixContainer() + .UntilCommandIsCompleted("mysql", "--host=localhost", $"--port={this.DefaultPort}", $"--user={this.Username}", $"--password={this.Password}", "--protocol=TCP", "--execute=SHOW DATABASES"); + } +} diff --git a/src/Testcontainers/Configurations/Modules/Databases/MsSqlTestcontainerConfiguration.cs b/src/Testcontainers/Configurations/Modules/Databases/MsSqlTestcontainerConfiguration.cs index 2d0c83046..a23e230a6 100644 --- a/src/Testcontainers/Configurations/Modules/Databases/MsSqlTestcontainerConfiguration.cs +++ b/src/Testcontainers/Configurations/Modules/Databases/MsSqlTestcontainerConfiguration.cs @@ -8,6 +8,8 @@ namespace DotNet.Testcontainers.Configurations [PublicAPI] public class MsSqlTestcontainerConfiguration : TestcontainerDatabaseConfiguration { + public const string MasterDatabase = "master"; + private const string MsSqlImage = "mcr.microsoft.com/mssql/server:2017-CU28-ubuntu-16.04"; private const int MsSqlPort = 1433; @@ -31,11 +33,8 @@ public MsSqlTestcontainerConfiguration(string image) } /// - public override string Database - { - get => "master"; - set => throw new NotImplementedException(); - } + public override string Database { get; set; } + = MasterDatabase; /// public override string Username diff --git a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs index 96de11eb0..1548425f9 100644 --- a/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs +++ b/src/Testcontainers/Configurations/PropertiesFileConfiguration.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Linq; + using System.Text.Json; using DotNet.Testcontainers.Images; /// @@ -10,6 +11,10 @@ /// internal sealed class PropertiesFileConfiguration : CustomConfiguration, ICustomConfiguration { + static PropertiesFileConfiguration() + { + } + /// /// Initializes a new instance of the class. /// @@ -29,6 +34,10 @@ public PropertiesFileConfiguration(string propertiesFilePath) { } + /// + /// Initializes a new instance of the class. + /// + /// A list of Java properties file lines. public PropertiesFileConfiguration(params string[] lines) : base(lines .Select(line => line.Trim()) @@ -41,6 +50,19 @@ public PropertiesFileConfiguration(params string[] lines) { } + /// + /// Gets the instance. + /// + public static ICustomConfiguration Instance { get; } + = new PropertiesFileConfiguration(); + + /// + public string GetDockerConfig() + { + const string propertyName = "docker.config"; + return this.GetDockerConfig(propertyName); + } + /// public Uri GetDockerHost() { @@ -48,6 +70,13 @@ public Uri GetDockerHost() return this.GetDockerHost(propertyName); } + /// + public JsonDocument GetDockerAuthConfig() + { + const string propertyName = "docker.auth.config"; + return this.GetDockerAuthConfig(propertyName); + } + /// public bool GetRyukDisabled() { diff --git a/src/Testcontainers/Configurations/TestcontainersSettings.cs b/src/Testcontainers/Configurations/TestcontainersSettings.cs index c567e671c..f155be722 100644 --- a/src/Testcontainers/Configurations/TestcontainersSettings.cs +++ b/src/Testcontainers/Configurations/TestcontainersSettings.cs @@ -18,11 +18,7 @@ namespace DotNet.Testcontainers.Configurations [PublicAPI] public static class TestcontainersSettings { - private static readonly IDockerImage RyukContainerImage = new DockerImage("ghcr.io/psanetra/ryuk:2021.12.20"); - - private static readonly ICustomConfiguration PropertiesFileConfiguration = new PropertiesFileConfiguration(); - - private static readonly ICustomConfiguration EnvironmentConfiguration = new EnvironmentConfiguration(); + private static readonly IDockerImage RyukContainerImage = new DockerImage("testcontainers/ryuk:0.3.4"); private static readonly IDockerEndpointAuthenticationConfiguration DockerEndpointAuthConfig = new IDockerEndpointAuthenticationProvider[] { new EnvironmentEndpointAuthenticationProvider(), new NpipeEndpointAuthenticationProvider(), new UnixEndpointAuthenticationProvider() } @@ -30,21 +26,21 @@ public static class TestcontainersSettings .Where(authProvider => authProvider.IsApplicable()) .Where(authProvider => authProvider.IsAvailable()) .Select(authProvider => authProvider.GetAuthConfig()) - .First(); + .FirstOrDefault(); /// /// Gets or sets a value indicating whether the is enabled or not. /// [PublicAPI] public static bool ResourceReaperEnabled { get; set; } - = !PropertiesFileConfiguration.GetRyukDisabled() && !EnvironmentConfiguration.GetRyukDisabled(); + = !PropertiesFileConfiguration.Instance.GetRyukDisabled() && !EnvironmentConfiguration.Instance.GetRyukDisabled(); /// /// Gets or sets the image. /// [PublicAPI] public static IDockerImage ResourceReaperImage { get; set; } - = PropertiesFileConfiguration.GetRyukContainerImage() ?? EnvironmentConfiguration.GetRyukContainerImage() ?? RyukContainerImage; + = PropertiesFileConfiguration.Instance.GetRyukContainerImage() ?? EnvironmentConfiguration.Instance.GetRyukContainerImage() ?? RyukContainerImage; /// /// Gets or sets the public host port. @@ -68,7 +64,7 @@ public static class TestcontainersSettings [PublicAPI] [CanBeNull] public static string HubImageNamePrefix { get; set; } - = PropertiesFileConfiguration.GetHubImageNamePrefix() ?? EnvironmentConfiguration.GetHubImageNamePrefix(); + = PropertiesFileConfiguration.Instance.GetHubImageNamePrefix() ?? EnvironmentConfiguration.Instance.GetHubImageNamePrefix(); /// /// Gets or sets the logger. diff --git a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs index bb2902491..ccce2f911 100644 --- a/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs +++ b/src/Testcontainers/Configurations/WaitStrategies/WaitStrategy.cs @@ -3,6 +3,7 @@ namespace DotNet.Testcontainers.Configurations using System; using System.Threading; using System.Threading.Tasks; + using JetBrains.Annotations; internal static class WaitStrategy { @@ -15,6 +16,7 @@ internal static class WaitStrategy /// Propagates notification that operations should be canceled. /// Thrown as soon as the timeout expires. /// A task that represents the asynchronous block operation. + [PublicAPI] public static async Task WaitWhile(Func> wait, int frequency = 25, int timeout = -1, CancellationToken ct = default) { var waitTask = Task.Run( @@ -48,6 +50,7 @@ await Task.Delay(frequency, ct) /// Propagates notification that operations should be canceled. /// Thrown as soon as the timeout expires. /// A task that represents the asynchronous block operation. + [PublicAPI] public static async Task WaitUntil(Func> wait, int frequency = 25, int timeout = -1, CancellationToken ct = default) { var waitTask = Task.Run( diff --git a/src/Testcontainers/Containers/IDockerContainer.cs b/src/Testcontainers/Containers/IDockerContainer.cs index 6bc0b0526..a4cb35980 100644 --- a/src/Testcontainers/Containers/IDockerContainer.cs +++ b/src/Testcontainers/Containers/IDockerContainer.cs @@ -10,6 +10,7 @@ namespace DotNet.Testcontainers.Containers /// /// This class represents a Docker container. /// + [PublicAPI] public interface IDockerContainer : IRunningDockerContainer, IAsyncDisposable { /// @@ -17,6 +18,7 @@ public interface IDockerContainer : IRunningDockerContainer, IAsyncDisposable /// /// Cancellation token. /// Returns the Docker container exit code. + [PublicAPI] Task GetExitCode(CancellationToken ct = default); /// @@ -30,6 +32,7 @@ public interface IDockerContainer : IRunningDockerContainer, IAsyncDisposable /// Thrown when a Docker API call gets canceled. /// Thrown when a Testcontainers task gets canceled. /// Thrown when the wait strategy task gets canceled or the timeout expires. + [PublicAPI] Task StartAsync(CancellationToken ct = default); /// @@ -39,12 +42,14 @@ public interface IDockerContainer : IRunningDockerContainer, IAsyncDisposable /// A task that represents the asynchronous stop operation of a Testcontainer. /// Thrown when a Docker API call gets canceled. /// Thrown when a Testcontainers task gets canceled. + [PublicAPI] Task StopAsync(CancellationToken ct = default); } /// /// This class represents a running Docker container. /// + [PublicAPI] public interface IRunningDockerContainer { /// @@ -54,6 +59,7 @@ public interface IRunningDockerContainer /// Returns the Docker container id if present or an empty string instead. /// /// If container was not created. + [PublicAPI] [NotNull] string Id { get; } @@ -64,6 +70,7 @@ public interface IRunningDockerContainer /// Returns the Docker container name if present or an empty string instead. /// /// If container was not created. + [PublicAPI] [NotNull] string Name { get; } @@ -74,6 +81,7 @@ public interface IRunningDockerContainer /// Returns the Docker container ip address if present or an empty string instead. /// /// If container was not created. + [PublicAPI] [NotNull] string IpAddress { get; } @@ -84,6 +92,7 @@ public interface IRunningDockerContainer /// Returns the Docker container mac address if present or an empty string instead. /// /// If container was not created. + [PublicAPI] [NotNull] string MacAddress { get; } @@ -94,6 +103,7 @@ public interface IRunningDockerContainer /// Returns the Docker container hostname if present or an empty string instead. /// /// If container was not created. + [PublicAPI] [NotNull] string Hostname { get; } @@ -103,6 +113,7 @@ public interface IRunningDockerContainer /// /// Returns the Docker image. /// + [PublicAPI] [NotNull] IDockerImage Image { get; } @@ -112,6 +123,7 @@ public interface IRunningDockerContainer /// /// Returns the Docker container state. /// + [PublicAPI] TestcontainersState State { get; } /// @@ -120,6 +132,7 @@ public interface IRunningDockerContainer /// Private container port. /// Returns the public host port associated with the private container port. /// If container was not created. + [PublicAPI] ushort GetMappedPublicPort(int privatePort); /// @@ -128,6 +141,7 @@ public interface IRunningDockerContainer /// Private container port. /// Returns the public host port associated with the private container port. /// If container was not created. + [PublicAPI] ushort GetMappedPublicPort(string privatePort); /// @@ -148,6 +162,7 @@ public interface IRunningDockerContainer ///
  • 644 octal 🠒 110_100_100 binary 🠒 420 decimal
  • /// /// + [PublicAPI] Task CopyFileAsync(string filePath, byte[] fileContent, int accessMode = 384, int userId = 0, int groupId = 0, CancellationToken ct = default); /// @@ -156,6 +171,7 @@ public interface IRunningDockerContainer /// Path to the file in the container. /// Cancellation token. /// Task that completes when the file has been read. + [PublicAPI] Task ReadFileAsync(string filePath, CancellationToken ct = default); /// @@ -164,6 +180,7 @@ public interface IRunningDockerContainer /// Shell command. /// Cancellation token. /// Task that completes when the shell command has been executed. + [PublicAPI] Task ExecAsync(IList command, CancellationToken ct = default); } } diff --git a/src/Testcontainers/Containers/ITestcontainersContainer.cs b/src/Testcontainers/Containers/ITestcontainersContainer.cs index 5f17ed1b0..f799f5ab9 100644 --- a/src/Testcontainers/Containers/ITestcontainersContainer.cs +++ b/src/Testcontainers/Containers/ITestcontainersContainer.cs @@ -1,5 +1,8 @@ namespace DotNet.Testcontainers.Containers { + using JetBrains.Annotations; + + [PublicAPI] public interface ITestcontainersContainer : IDockerContainer { } diff --git a/src/Testcontainers/Containers/Modules/Databases/AzuriteTestcontainer.cs b/src/Testcontainers/Containers/Modules/Databases/AzuriteTestcontainer.cs new file mode 100644 index 000000000..98dd094da --- /dev/null +++ b/src/Testcontainers/Containers/Modules/Databases/AzuriteTestcontainer.cs @@ -0,0 +1,107 @@ +namespace DotNet.Testcontainers.Containers +{ + using System; + using System.Collections.Generic; + using System.Linq; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + using Microsoft.Extensions.Logging; + + /// + /// This class represents an extended configured Testcontainer for Azurite. + /// + [PublicAPI] + public sealed class AzuriteTestcontainer : TestcontainersContainer + { + /// + /// Initializes a new instance of the class. + /// + /// The Testcontainers configuration. + /// The logger. + internal AzuriteTestcontainer(ITestcontainersConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + /// Gets the host blob port. + /// + [PublicAPI] + public int BlobPort + => this.GetMappedPublicPort(this.ContainerBlobPort); + + /// + /// Gets the host queue port. + /// + [PublicAPI] + public int QueuePort + => this.GetMappedPublicPort(this.ContainerQueuePort); + + /// + /// Gets the host table port. + /// + [PublicAPI] + public int TablePort + => this.GetMappedPublicPort(this.ContainerTablePort); + + /// + /// Gets or sets the container blob port. + /// + [PublicAPI] + public int ContainerBlobPort { get; set; } + + /// + /// Gets or sets the container queue port. + /// + [PublicAPI] + public int ContainerQueuePort { get; set; } + + /// + /// Gets or sets the container table port. + /// + [PublicAPI] + public int ContainerTablePort { get; set; } + + /// + /// Gets the Storage connection string. + /// + [PublicAPI] + public string ConnectionString + { + get + { + // https://docs.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio#well-known-storage-account-and-key. + const string accountName = "devstoreaccount1"; + + const string accountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; + + var endpointBuilder = new UriBuilder("http", this.Hostname, -1, accountName); + + IDictionary connectionString = new Dictionary(); + connectionString.Add("DefaultEndpointsProtocol", endpointBuilder.Scheme); + connectionString.Add("AccountName", accountName); + connectionString.Add("AccountKey", accountKey); + + if (this.ContainerBlobPort > 0) + { + endpointBuilder.Port = this.BlobPort; + connectionString.Add("BlobEndpoint", endpointBuilder.ToString()); + } + + if (this.ContainerQueuePort > 0) + { + endpointBuilder.Port = this.QueuePort; + connectionString.Add("QueueEndpoint", endpointBuilder.ToString()); + } + + if (this.ContainerTablePort > 0) + { + endpointBuilder.Port = this.TablePort; + connectionString.Add("TableEndpoint", endpointBuilder.ToString()); + } + + return string.Join(";", connectionString.Select(kvp => $"{kvp.Key}={kvp.Value}")); + } + } + } +} diff --git a/src/Testcontainers/Containers/Modules/Databases/ElasticsearchTestcontainer.cs b/src/Testcontainers/Containers/Modules/Databases/ElasticsearchTestcontainer.cs new file mode 100644 index 000000000..26826ff35 --- /dev/null +++ b/src/Testcontainers/Containers/Modules/Databases/ElasticsearchTestcontainer.cs @@ -0,0 +1,25 @@ +namespace DotNet.Testcontainers.Containers +{ + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + using Microsoft.Extensions.Logging; + + /// + [PublicAPI] + public sealed class ElasticsearchTestcontainer : TestcontainerDatabase + { + /// + /// Initializes a new instance of the class. + /// + /// The Testcontainers configuration. + /// The logger. + internal ElasticsearchTestcontainer(ITestcontainersConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + public override string ConnectionString + => $"https://{this.Hostname}:{this.Port}"; + } +} diff --git a/src/Testcontainers/Containers/Modules/Databases/MariaDbTestcontainer.cs b/src/Testcontainers/Containers/Modules/Databases/MariaDbTestcontainer.cs new file mode 100644 index 000000000..3935669ce --- /dev/null +++ b/src/Testcontainers/Containers/Modules/Databases/MariaDbTestcontainer.cs @@ -0,0 +1,45 @@ +namespace DotNet.Testcontainers.Containers +{ + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; + using Microsoft.Extensions.Logging; + + /// + [PublicAPI] + public sealed class MariaDbTestcontainer : TestcontainerDatabase + { + /// + /// Initializes a new instance of the class. + /// + /// The Testcontainers configuration. + /// The logger. + internal MariaDbTestcontainer(ITestcontainersConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + } + + /// + public override string ConnectionString + => $"Server={this.Hostname};Port={this.Port};Database={this.Database};Uid={this.Username};Pwd={this.Password};"; + + /// + /// Executes a SQL script in the database container. + /// + /// The content of the SQL script to be executed. + /// Cancellation token. + /// Task that completes when the script has been executed. + public override async Task ExecScriptAsync(string scriptContent, CancellationToken ct = default) + { + var tempScriptFile = this.GetTempScriptFile(); + + await this.CopyFileAsync(tempScriptFile, Encoding.Default.GetBytes(scriptContent), 493, 0, 0, ct) + .ConfigureAwait(false); + + return await this.ExecAsync(new[] { "mysql", $"--host={this.Hostname}", $"--port={this.ContainerPort}", $"--user={this.Username}", $"--password={this.Password}", this.Database, $"--execute=source {tempScriptFile}" }, ct) + .ConfigureAwait(false); + } + } +} diff --git a/src/Testcontainers/Containers/Modules/Databases/MongoDbTestcontainer.cs b/src/Testcontainers/Containers/Modules/Databases/MongoDbTestcontainer.cs index 733d891cb..7a2e74737 100644 --- a/src/Testcontainers/Containers/Modules/Databases/MongoDbTestcontainer.cs +++ b/src/Testcontainers/Containers/Modules/Databases/MongoDbTestcontainer.cs @@ -20,6 +20,8 @@ internal MongoDbTestcontainer(ITestcontainersConfiguration configuration, ILogge /// public override string ConnectionString - => $"mongodb://{this.Username}:{this.Password}@{this.Hostname}:{this.Port}"; + => string.IsNullOrEmpty(this.Username) && string.IsNullOrEmpty(this.Password) + ? $"mongodb://{this.Hostname}:{this.Port}" + : $"mongodb://{this.Username}:{this.Password}@{this.Hostname}:{this.Port}"; } } diff --git a/src/Testcontainers/Containers/Modules/Databases/MsSqlTestcontainer.cs b/src/Testcontainers/Containers/Modules/Databases/MsSqlTestcontainer.cs index 07799509d..3c26aa440 100644 --- a/src/Testcontainers/Containers/Modules/Databases/MsSqlTestcontainer.cs +++ b/src/Testcontainers/Containers/Modules/Databases/MsSqlTestcontainer.cs @@ -1,5 +1,6 @@ namespace DotNet.Testcontainers.Containers { + using System; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -41,5 +42,29 @@ await this.CopyFileAsync(tempScriptFile, Encoding.Default.GetBytes(scriptContent return await this.ExecAsync(new[] { "/opt/mssql-tools/bin/sqlcmd", "-b", "-r", "1", "-S", $"{this.Hostname},{this.ContainerPort}", "-U", this.Username, "-P", this.Password, "-i", tempScriptFile }, ct) .ConfigureAwait(false); } + + /// + public override async Task StartAsync(CancellationToken ct = default) + { + await base.StartAsync(ct) + .ConfigureAwait(false); + + // MSSQL contains the master database by default. It's not necessary to create it. + if (MsSqlTestcontainerConfiguration.MasterDatabase.Equals(this.Database, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // Replace this with proper SQL args, soon as we moved to dedicated modules. + var createDatabaseScript = $@" + IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '{this.Database}') + BEGIN + CREATE DATABASE [{this.Database}]; + END; + "; + + await this.ExecScriptAsync(createDatabaseScript, ct) + .ConfigureAwait(false); + } } } diff --git a/src/Testcontainers/Containers/Modules/TestcontainerDatabase.cs b/src/Testcontainers/Containers/Modules/TestcontainerDatabase.cs index 6b0f2bcd5..ddcc7edde 100644 --- a/src/Testcontainers/Containers/Modules/TestcontainerDatabase.cs +++ b/src/Testcontainers/Containers/Modules/TestcontainerDatabase.cs @@ -11,6 +11,7 @@ namespace DotNet.Testcontainers.Containers /// /// This class represents an extended configured Testcontainer for databases. /// + [PublicAPI] public abstract class TestcontainerDatabase : HostedServiceContainer, IDatabaseScript { /// @@ -39,6 +40,7 @@ protected TestcontainerDatabase(ITestcontainersConfiguration configuration, ILog /// Creates a path to a temporary script file. /// /// A path to a temporary script file. + [PublicAPI] public virtual string GetTempScriptFile() { return Path.Combine("/tmp/", Path.GetRandomFileName()); diff --git a/src/Testcontainers/Containers/ResourceReaper.cs b/src/Testcontainers/Containers/ResourceReaper.cs index 67775aaab..9c7c59c6a 100644 --- a/src/Testcontainers/Containers/ResourceReaper.cs +++ b/src/Testcontainers/Containers/ResourceReaper.cs @@ -21,8 +21,14 @@ public sealed class ResourceReaper : IAsyncDisposable private const ushort RyukPort = 8080; + private const int ConnectionTimeoutInSeconds = 60; + + private const int RetryTimeoutInSeconds = 2; + private static readonly SemaphoreSlim DefaultLock = new SemaphoreSlim(1, 1); + private static readonly LingerOption DiscardAllPendingData = new LingerOption(true, 0); + private static ResourceReaper defaultInstance; private readonly CancellationTokenSource maintainConnectionCts = new CancellationTokenSource(); @@ -33,6 +39,10 @@ public sealed class ResourceReaper : IAsyncDisposable private bool disposed; + static ResourceReaper() + { + } + private ResourceReaper(Guid sessionId, IDockerEndpointAuthenticationConfiguration dockerEndpointAuthConfig, string ryukImage) { dockerEndpointAuthConfig = dockerEndpointAuthConfig ?? TestcontainersSettings.OS.DockerEndpointAuthConfig; @@ -146,7 +156,7 @@ public static async Task GetAndStartNewAsync(Guid sessionId, IDo var resourceReaper = new ResourceReaper(sessionId, dockerEndpointAuthConfig, ryukImage); - initTimeout = TimeSpan.Equals(default, initTimeout) ? TimeSpan.FromSeconds(10) : initTimeout; + initTimeout = TimeSpan.Equals(default, initTimeout) ? TimeSpan.FromSeconds(ConnectionTimeoutInSeconds) : initTimeout; try { @@ -250,11 +260,11 @@ private bool TryGetEndpoint(out string host, out ushort port) /// The cancellation token to cancel the initialization. This will not cancel the maintained connection. private async Task MaintainRyukConnection(TaskCompletionSource ryukInitializedTaskSource, CancellationToken ct) { - while (!this.maintainConnectionCts.IsCancellationRequested && (!ct.IsCancellationRequested || ryukInitializedTaskSource.Task.IsCompleted)) + connect_to_ryuk: while (!this.maintainConnectionCts.IsCancellationRequested && !ct.IsCancellationRequested && !ryukInitializedTaskSource.Task.IsCompleted) { if (!this.TryGetEndpoint(out var host, out var port)) { - await Task.Delay(TimeSpan.FromSeconds(1), default) + await Task.Delay(TimeSpan.FromSeconds(RetryTimeoutInSeconds), default) .ConfigureAwait(false); continue; @@ -262,6 +272,8 @@ await Task.Delay(TimeSpan.FromSeconds(1), default) using (var tcpClient = new TcpClient()) { + tcpClient.LingerState = DiscardAllPendingData; + try { await tcpClient.ConnectAsync(host, port) @@ -302,6 +314,20 @@ await stream.FlushAsync(ct) .ConfigureAwait(false); #endif + if (numberOfBytes == 0) + { + // Even if there is no listening socket behind the bound port, the TcpClient establishes a connection. + // If we do not receive any data, the socket is not ready yet. + await Task.Delay(TimeSpan.FromSeconds(RetryTimeoutInSeconds), ct) + .ConfigureAwait(false); + +#pragma warning disable S907 + + goto connect_to_ryuk; + +#pragma warning restore S907 + } + var indexOfNewLine = Array.IndexOf(readBytes, (byte)'\n'); if (indexOfNewLine == -1) @@ -343,14 +369,14 @@ await stream.FlushAsync(ct) { this.resourceReaperContainer.Logger.CanNotConnectToResourceReaper(this.SessionId, host, port, e); - await Task.Delay(TimeSpan.FromSeconds(1), default) + await Task.Delay(TimeSpan.FromSeconds(RetryTimeoutInSeconds), default) .ConfigureAwait(false); } catch (Exception e) { this.resourceReaperContainer.Logger.LostConnectionToResourceReaper(this.SessionId, host, port, e); - await Task.Delay(TimeSpan.FromSeconds(1), default) + await Task.Delay(TimeSpan.FromSeconds(RetryTimeoutInSeconds), default) .ConfigureAwait(false); } } diff --git a/src/Testcontainers/Containers/TestcontainersContainer.cs b/src/Testcontainers/Containers/TestcontainersContainer.cs index 5ae9a63c8..8fdcb4ca8 100644 --- a/src/Testcontainers/Containers/TestcontainersContainer.cs +++ b/src/Testcontainers/Containers/TestcontainersContainer.cs @@ -38,7 +38,7 @@ public class TestcontainersContainer : ITestcontainersContainer /// The logger. protected TestcontainersContainer(ITestcontainersConfiguration configuration, ILogger logger) { - this.client = new TestcontainersClient(configuration.DockerEndpointAuthConfig, logger); + this.client = new TestcontainersClient(configuration.SessionId, configuration.DockerEndpointAuthConfig, logger); this.configuration = configuration; this.Logger = logger; } @@ -257,7 +257,7 @@ public async ValueTask DisposeAsync() } // If someone calls `DisposeAsync`, we can immediately remove the container. We don't need to wait for the Resource Reaper. - if (!Guid.Empty.ToString("D").Equals(this.configuration.Labels[ResourceReaper.ResourceReaperSessionLabel], StringComparison.OrdinalIgnoreCase)) + if (!Guid.Empty.Equals(this.configuration.SessionId)) { await this.CleanUpAsync() .ConfigureAwait(false); diff --git a/src/Testcontainers/Containers/TestcontainersState.cs b/src/Testcontainers/Containers/TestcontainersState.cs index a79616002..017614bc8 100644 --- a/src/Testcontainers/Containers/TestcontainersState.cs +++ b/src/Testcontainers/Containers/TestcontainersState.cs @@ -1,43 +1,53 @@ namespace DotNet.Testcontainers.Containers { + using JetBrains.Annotations; + /// /// Docker container states. /// + [PublicAPI] public enum TestcontainersState { /// /// Docker container was not created. /// + [PublicAPI] Undefined, /// /// Docker container is created. /// + [PublicAPI] Created, /// /// Docker container is restarting. /// + [PublicAPI] Restarting, /// /// Docker container is running. /// + [PublicAPI] Running, /// /// Docker container is paused. /// + [PublicAPI] Paused, /// /// Docker container is exited. /// + [PublicAPI] Exited, /// /// Docker container is dead. /// + [PublicAPI] Dead, } } diff --git a/src/Testcontainers/Guard.Null.cs b/src/Testcontainers/Guard.Null.cs index b9e5a8626..8958ddb4a 100644 --- a/src/Testcontainers/Guard.Null.cs +++ b/src/Testcontainers/Guard.Null.cs @@ -2,6 +2,8 @@ namespace DotNet.Testcontainers { using System; using System.Diagnostics; + using DotNet.Testcontainers.Configurations; + using JetBrains.Annotations; /// /// Nullability preconditions. @@ -15,6 +17,7 @@ internal static partial class Guard /// Type of the argument. /// Reference to the Guard object that validates the argument preconditions. /// Thrown when argument is not null. + [PublicAPI] [DebuggerStepThrough] public static ref readonly ArgumentInfo Null(in this ArgumentInfo argument) where TType : class @@ -34,6 +37,7 @@ public static ref readonly ArgumentInfo Null(in this ArgumentInfo< /// Type of the argument. /// Reference to the Guard object that validates the argument preconditions. /// Thrown when argument is null. + [PublicAPI] [DebuggerStepThrough] public static ref readonly ArgumentInfo NotNull(in this ArgumentInfo argument) where TType : class @@ -45,5 +49,26 @@ public static ref readonly ArgumentInfo NotNull(in this ArgumentIn return ref argument; } + + /// + /// Ensures that the Docker endpoint authentication configuration is set. + /// + /// The Docker endpoint authentication configuration. + /// An implementation of . + /// Reference to the Guard object that validates the argument preconditions. + /// Thrown when argument is null. + [PublicAPI] + [DebuggerStepThrough] + public static ref readonly ArgumentInfo DockerEndpointAuthConfigIsSet(in this ArgumentInfo argument) + where TType : IDockerEndpointAuthenticationConfiguration + { + if (argument.HasValue()) + { + return ref argument; + } + + const string message = "Cannot detect the Docker endpoint. Use either the environment variables or the ~/.testcontainers.properties file to customize your configuration:\nhttps://www.testcontainers.org/features/configuration/#customizing-docker-host-detection"; + throw new ArgumentNullException(argument.Name, message); + } } } diff --git a/src/Testcontainers/Guard.String.cs b/src/Testcontainers/Guard.String.cs index d038fb8e9..0cb1b9194 100644 --- a/src/Testcontainers/Guard.String.cs +++ b/src/Testcontainers/Guard.String.cs @@ -3,6 +3,7 @@ namespace DotNet.Testcontainers using System; using System.Diagnostics; using System.Linq; + using JetBrains.Annotations; /// /// String preconditions. @@ -15,6 +16,7 @@ internal static partial class Guard /// String argument to validate. /// Reference to the Guard object that validates the argument preconditions. /// Thrown when argument is not empty. + [PublicAPI] [DebuggerStepThrough] public static ref readonly ArgumentInfo Empty(in this ArgumentInfo argument) { @@ -32,6 +34,7 @@ public static ref readonly ArgumentInfo Empty(in this ArgumentInfoString argument to validate. /// Reference to the Guard object that validates the argument preconditions. /// Thrown when argument is empty. + [PublicAPI] [DebuggerStepThrough] public static ref readonly ArgumentInfo NotEmpty(in this ArgumentInfo argument) { @@ -49,6 +52,7 @@ public static ref readonly ArgumentInfo NotEmpty(in this ArgumentInfoString argument to validate. /// Reference to the Guard object that validates the argument preconditions. /// Thrown when argument has uppercase characters. + [PublicAPI] [DebuggerStepThrough] public static ref readonly ArgumentInfo NotUppercase(in this ArgumentInfo argument) { diff --git a/src/Testcontainers/Images/DockerImage.cs b/src/Testcontainers/Images/DockerImage.cs index fdbcc7dfa..0b4510f6f 100644 --- a/src/Testcontainers/Images/DockerImage.cs +++ b/src/Testcontainers/Images/DockerImage.cs @@ -2,8 +2,10 @@ namespace DotNet.Testcontainers.Images { using System; using System.Linq; + using JetBrains.Annotations; /// + [PublicAPI] public sealed class DockerImage : IDockerImage { private static readonly Func GetDockerImage = MatchImage.Match; @@ -14,6 +16,7 @@ public sealed class DockerImage : IDockerImage /// Initializes a new instance of the class. /// /// The Docker image. + [PublicAPI] public DockerImage(IDockerImage image) : this(image.Repository, image.Name, image.Tag) { @@ -25,6 +28,7 @@ public DockerImage(IDockerImage image) /// The Docker image. /// Thrown when any argument is null. /// "fedora/httpd:version1.0" where "fedora" is the repository, "httpd" the name and "version1.0" the tag. + [PublicAPI] public DockerImage(string image) : this(GetDockerImage(image)) { @@ -39,6 +43,7 @@ public DockerImage(string image) /// The Docker Hub image name prefix. /// Thrown when any argument is null. /// "fedora/httpd:version1.0" where "fedora" is the repository, "httpd" the name and "version1.0" the tag. + [PublicAPI] public DockerImage( string repository, string name, diff --git a/src/Testcontainers/Images/IDockerImage.cs b/src/Testcontainers/Images/IDockerImage.cs index 209dd267b..020601749 100644 --- a/src/Testcontainers/Images/IDockerImage.cs +++ b/src/Testcontainers/Images/IDockerImage.cs @@ -5,23 +5,27 @@ namespace DotNet.Testcontainers.Images /// /// This class represents a Docker image. /// + [PublicAPI] public interface IDockerImage { /// /// Gets the Docker image repository name. /// + [PublicAPI] [NotNull] string Repository { get; } /// /// Gets the Docker image name. /// + [PublicAPI] [NotNull] string Name { get; } /// /// Gets the Docker image tag. /// + [PublicAPI] [NotNull] string Tag { get; } @@ -31,6 +35,7 @@ public interface IDockerImage /// /// Full Docker image name, like "foo/bar:1.0.0" or "bar:latest" based on the components values. /// + [PublicAPI] [NotNull] string FullName { get; } @@ -38,6 +43,7 @@ public interface IDockerImage /// Gets the Docker registry hostname. /// /// The Docker registry hostname. + [PublicAPI] [CanBeNull] string GetHostname(); } diff --git a/src/Testcontainers/Networks/IDockerNetwork.cs b/src/Testcontainers/Networks/IDockerNetwork.cs index d67beec5e..3d8bcf8e4 100644 --- a/src/Testcontainers/Networks/IDockerNetwork.cs +++ b/src/Testcontainers/Networks/IDockerNetwork.cs @@ -2,20 +2,24 @@ namespace DotNet.Testcontainers.Networks { using System.Threading; using System.Threading.Tasks; + using JetBrains.Annotations; /// /// A Docker network. /// + [PublicAPI] public interface IDockerNetwork { /// /// Gets the Docker network id. /// + [PublicAPI] string Id { get; } /// /// Gets the Docker network name. /// + [PublicAPI] string Name { get; } /// @@ -23,6 +27,7 @@ public interface IDockerNetwork /// /// Cancellation token. /// Task that completes when the network has been created. + [PublicAPI] Task CreateAsync(CancellationToken ct = default); /// @@ -30,6 +35,7 @@ public interface IDockerNetwork /// /// Cancellation token. /// Task that completes when the network has been deleted. + [PublicAPI] Task DeleteAsync(CancellationToken ct = default); } } diff --git a/src/Testcontainers/Networks/NonExistingDockerNetwork.cs b/src/Testcontainers/Networks/NonExistingDockerNetwork.cs index deb4d304c..f509f6800 100644 --- a/src/Testcontainers/Networks/NonExistingDockerNetwork.cs +++ b/src/Testcontainers/Networks/NonExistingDockerNetwork.cs @@ -26,7 +26,7 @@ internal sealed class NonExistingDockerNetwork : IDockerNetwork /// The logger. public NonExistingDockerNetwork(ITestcontainersNetworkConfiguration configuration, ILogger logger) { - this.client = new DockerNetworkOperations(configuration.DockerEndpointAuthConfig, logger); + this.client = new DockerNetworkOperations(configuration.SessionId, configuration.DockerEndpointAuthConfig, logger); this.configuration = configuration; } diff --git a/src/Testcontainers/Volumes/IDockerVolume.cs b/src/Testcontainers/Volumes/IDockerVolume.cs index 19beaa8f0..c6be9d3ff 100644 --- a/src/Testcontainers/Volumes/IDockerVolume.cs +++ b/src/Testcontainers/Volumes/IDockerVolume.cs @@ -2,15 +2,18 @@ namespace DotNet.Testcontainers.Volumes { using System.Threading; using System.Threading.Tasks; + using JetBrains.Annotations; /// /// A Docker volume. /// + [PublicAPI] public interface IDockerVolume { /// /// Gets the Docker volume name. /// + [PublicAPI] string Name { get; } /// @@ -18,6 +21,7 @@ public interface IDockerVolume /// /// Cancellation token. /// Task that completes when the volume has been created. + [PublicAPI] Task CreateAsync(CancellationToken ct = default); /// @@ -25,6 +29,7 @@ public interface IDockerVolume /// /// Cancellation token. /// Task that completes when the volume has been deleted. + [PublicAPI] Task DeleteAsync(CancellationToken ct = default); } } diff --git a/src/Testcontainers/Volumes/NonExistingDockerVolume.cs b/src/Testcontainers/Volumes/NonExistingDockerVolume.cs index 66959028b..843830ce3 100644 --- a/src/Testcontainers/Volumes/NonExistingDockerVolume.cs +++ b/src/Testcontainers/Volumes/NonExistingDockerVolume.cs @@ -26,7 +26,7 @@ internal sealed class NonExistingDockerVolume : IDockerVolume /// The logger. public NonExistingDockerVolume(ITestcontainersVolumeConfiguration configuration, ILogger logger) { - this.client = new DockerVolumeOperations(configuration.DockerEndpointAuthConfig, logger); + this.client = new DockerVolumeOperations(configuration.SessionId, configuration.DockerEndpointAuthConfig, logger); this.configuration = configuration; } diff --git a/tests/Testcontainers.ResourceReaper.Tests/Testcontainers.ResourceReaper.Tests.csproj b/tests/Testcontainers.ResourceReaper.Tests/Testcontainers.ResourceReaper.Tests.csproj index f1c0ca975..2657e42f2 100644 --- a/tests/Testcontainers.ResourceReaper.Tests/Testcontainers.ResourceReaper.Tests.csproj +++ b/tests/Testcontainers.ResourceReaper.Tests/Testcontainers.ResourceReaper.Tests.csproj @@ -17,6 +17,6 @@ - + diff --git a/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.bat b/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.bat index 46117b80c..60626a3a4 100644 --- a/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.bat +++ b/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.bat @@ -1,3 +1,3 @@ @echo off set /P hostname= -echo {"ServerAddress":"https://index.docker.io/v1/","Username":"username","Password":"password"} +echo {"ServerURL":"https://index.docker.io/v1/","Username":"username","Secret":"password"} diff --git a/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.sh b/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.sh index 3fb6360e6..76a1caeae 100755 --- a/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.sh +++ b/tests/Testcontainers.Tests/Assets/credsStore/docker-credential-desktop.sh @@ -1,3 +1,3 @@ #!/bin/bash read -echo '{"ServerAddress":"https://index.docker.io/v1/","Username":"username","Password":"password"}' +echo '{"ServerURL":"https://index.docker.io/v1/","Username":"username","Secret":"password"}' diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/AlpineFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/AlpineFixture.cs index 9515736e1..a98b2186f 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/AlpineFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/AlpineFixture.cs @@ -4,8 +4,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using System.Threading.Tasks; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using Xunit; + [UsedImplicitly] public sealed class AlpineFixture : IAsyncLifetime { public ITestcontainersContainer Container { get; } diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/AzuriteFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/AzuriteFixture.cs new file mode 100644 index 000000000..69a752589 --- /dev/null +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/AzuriteFixture.cs @@ -0,0 +1,113 @@ +namespace DotNet.Testcontainers.Tests.Fixtures +{ + using System; + using System.IO; + using System.Threading.Tasks; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + using Xunit; + + public static class AzuriteFixture + { + // We cannot use `Path.GetTempPath()` on macOS, see: https://github.com/common-workflow-language/cwltool/issues/328. + private static readonly string TempDir = Environment.GetEnvironmentVariable("AGENT_TEMPDIRECTORY") ?? Directory.GetCurrentDirectory(); + + [UsedImplicitly] + public class AzuriteDefaultFixture : IAsyncLifetime + { + public AzuriteDefaultFixture() + : this(new AzuriteTestcontainerConfiguration()) + { + } + + protected AzuriteDefaultFixture(AzuriteTestcontainerConfiguration configuration) + { + this.Configuration = configuration; + this.Container = new TestcontainersBuilder() + .WithAzurite(configuration) + .Build(); + } + + public AzuriteTestcontainerConfiguration Configuration { get; } + + public AzuriteTestcontainer Container { get; } + + public Task InitializeAsync() + { + return this.Container.StartAsync(); + } + + public Task DisposeAsync() + { + return this.Container.DisposeAsync().AsTask(); + } + } + + [UsedImplicitly] + public sealed class AzuriteWithBlobOnlyFixture : AzuriteDefaultFixture + { + public AzuriteWithBlobOnlyFixture() + : base(new AzuriteTestcontainerConfiguration { BlobServiceOnlyEnabled = true }) + { + } + } + + [UsedImplicitly] + public sealed class AzuriteWithQueueOnlyFixture : AzuriteDefaultFixture + { + public AzuriteWithQueueOnlyFixture() + : base(new AzuriteTestcontainerConfiguration { QueueServiceOnlyEnabled = true }) + { + } + } + + [UsedImplicitly] + public sealed class AzuriteWithTableOnlyFixture : AzuriteDefaultFixture + { + public AzuriteWithTableOnlyFixture() + : base(new AzuriteTestcontainerConfiguration { TableServiceOnlyEnabled = true }) + { + } + } + + [UsedImplicitly] + public sealed class AzuriteWithCustomContainerPortsFixture : AzuriteDefaultFixture + { + public AzuriteWithCustomContainerPortsFixture() + : base(new AzuriteTestcontainerConfiguration + { + BlobContainerPort = 65501, + QueueContainerPort = 65502, + TableContainerPort = 65503, + }) + { + } + } + + [UsedImplicitly] + public sealed class AzuriteWithCustomWorkspaceFixture : AzuriteDefaultFixture, IDisposable + { + public AzuriteWithCustomWorkspaceFixture() + : base(new AzuriteTestcontainerConfiguration + { + Location = Path.Combine(TempDir, Guid.NewGuid().ToString("N")), + }) + { + if (this.Configuration.Location != null) + { + Directory.CreateDirectory(this.Configuration.Location); + } + } + + public void Dispose() + { + if (Directory.Exists(this.Configuration.Location)) + { + Directory.Delete(this.Configuration.Location, true); + } + } + } + } +} diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchDbFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchDbFixture.cs index 675b65105..17f760f8c 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchDbFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchDbFixture.cs @@ -4,8 +4,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using MyCouch; + [UsedImplicitly] public sealed class CouchDbFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new CouchDbTestcontainerConfiguration { Database = "db", Username = "couchdb", Password = "couchdb" }; diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchbaseFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchbaseFixture.cs index eb5b09f28..3f85b8ae1 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchbaseFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/CouchbaseFixture.cs @@ -5,7 +5,9 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + [UsedImplicitly] public sealed class CouchbaseFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new CouchbaseTestcontainerConfiguration diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/ElasticsearchFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/ElasticsearchFixture.cs new file mode 100644 index 000000000..18bd3e43f --- /dev/null +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/ElasticsearchFixture.cs @@ -0,0 +1,47 @@ +namespace DotNet.Testcontainers.Tests.Fixtures +{ + using System; + using System.Threading.Tasks; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using Elastic.Clients.Elasticsearch; + using Elastic.Transport; + using JetBrains.Annotations; + + [UsedImplicitly] + public sealed class ElasticsearchFixture : DatabaseFixture + { + private readonly TestcontainerDatabaseConfiguration configuration = new ElasticsearchTestcontainerConfiguration { Password = "secret" }; + + public ElasticsearchFixture() + { + this.Container = new TestcontainersBuilder() + .WithDatabase(this.configuration) + .Build(); + } + + public override async Task InitializeAsync() + { + await this.Container.StartAsync() + .ConfigureAwait(false); + + var settings = new ElasticsearchClientSettings(new Uri(this.Container.ConnectionString)) + .ServerCertificateValidationCallback(CertificateValidations.AllowAll) + .Authentication(new BasicAuthentication(this.Container.Username, this.Container.Password)); + + this.Connection = new ElasticsearchClient(settings); + } + + public override async Task DisposeAsync() + { + await this.Container.DisposeAsync() + .ConfigureAwait(false); + } + + public override void Dispose() + { + this.configuration.Dispose(); + } + } +} diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MariaDbFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MariaDbFixture.cs new file mode 100644 index 000000000..b474897f4 --- /dev/null +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MariaDbFixture.cs @@ -0,0 +1,44 @@ +namespace DotNet.Testcontainers.Tests.Fixtures +{ + using System.Data.Common; + using System.Threading.Tasks; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + using MySqlConnector; + + [UsedImplicitly] + public sealed class MariaDbFixture : DatabaseFixture + { + private readonly TestcontainerDatabaseConfiguration configuration = new MariaDbTestcontainerConfiguration { Database = "db", Username = "mysql", Password = "mysql" }; + + public MariaDbFixture() + { + this.Container = new TestcontainersBuilder() + .WithDatabase(this.configuration) + .Build(); + } + + public override async Task InitializeAsync() + { + await this.Container.StartAsync() + .ConfigureAwait(false); + + this.Connection = new MySqlConnection(this.Container.ConnectionString); + } + + public override async Task DisposeAsync() + { + this.Connection.Dispose(); + + await this.Container.DisposeAsync() + .ConfigureAwait(false); + } + + public override void Dispose() + { + this.configuration.Dispose(); + } + } +} diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbFixture.cs index e923be5c2..5326347f7 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbFixture.cs @@ -4,11 +4,13 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using MongoDB.Driver; + [UsedImplicitly] public sealed class MongoDbFixture : DatabaseFixture { - private readonly TestcontainerDatabaseConfiguration configuration = new MongoDbTestcontainerConfiguration { Username = "mongoadmin", Password = "secret", Database = "admin" }; // https://hub.docker.com/_/mongo + private readonly TestcontainerDatabaseConfiguration configuration = new MongoDbTestcontainerConfiguration { Database = "db", Username = "mongo", Password = "mongo" }; public MongoDbFixture() { diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbNoAuthFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbNoAuthFixture.cs new file mode 100644 index 000000000..597d61f80 --- /dev/null +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MongoDbNoAuthFixture.cs @@ -0,0 +1,37 @@ +namespace DotNet.Testcontainers.Tests.Fixtures +{ + using System.Threading.Tasks; + using DotNet.Testcontainers.Builders; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + using MongoDB.Driver; + + [UsedImplicitly] + public sealed class MongoDbNoAuthFixture : DatabaseFixture + { + private readonly TestcontainerDatabaseConfiguration configuration = new MongoDbTestcontainerConfiguration { Database = "db", Username = null, Password = null }; + + public MongoDbNoAuthFixture() + { + this.Container = new TestcontainersBuilder() + .WithDatabase(this.configuration) + .Build(); + } + + public override Task InitializeAsync() + { + return this.Container.StartAsync(); + } + + public override Task DisposeAsync() + { + return this.Container.DisposeAsync().AsTask(); + } + + public override void Dispose() + { + this.configuration.Dispose(); + } + } +} diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MsSqlFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MsSqlFixture.cs index f469ef0f8..dfc53ce60 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MsSqlFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MsSqlFixture.cs @@ -6,10 +6,12 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; + [UsedImplicitly] public sealed class MsSqlFixture : DatabaseFixture { - private readonly TestcontainerDatabaseConfiguration configuration = new MsSqlTestcontainerConfiguration { Password = "yourStrong(!)Password" }; // https://hub.docker.com/r/microsoft/mssql-server-linux/. + private readonly TestcontainerDatabaseConfiguration configuration = new MsSqlTestcontainerConfiguration { Database = "db", Password = "yourStrong(!)Password" }; // https://hub.docker.com/r/microsoft/mssql-server-linux/. public MsSqlFixture() { diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MySqlFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MySqlFixture.cs index d17569522..08f84c432 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MySqlFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/MySqlFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; - using MySql.Data.MySqlClient; + using JetBrains.Annotations; + using MySqlConnector; + [UsedImplicitly] public sealed class MySqlFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new MySqlTestcontainerConfiguration { Database = "db", Username = "mysql", Password = "mysql" }; diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/OracleFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/OracleFixture.cs index 04e979aab..2272a260e 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/OracleFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/OracleFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using Oracle.ManagedDataAccess.Client; + [UsedImplicitly] public sealed class OracleFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new OracleTestcontainerConfiguration(); diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/PostgreSqlFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/PostgreSqlFixture.cs index 3cd955ce2..3232b8a90 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/PostgreSqlFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/PostgreSqlFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using Npgsql; + [UsedImplicitly] public sealed class PostgreSqlFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new PostgreSqlTestcontainerConfiguration { Database = "db", Username = "postgres", Password = "postgres" }; diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/RedisFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/RedisFixture.cs index a40a18316..beb53927f 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/RedisFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/Databases/RedisFixture.cs @@ -4,8 +4,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using StackExchange.Redis; + [UsedImplicitly] public sealed class RedisFixture : DatabaseFixture { private readonly TestcontainerDatabaseConfiguration configuration = new RedisTestcontainerConfiguration(); diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/KafkaFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/KafkaFixture.cs index 5b6a934b1..dd4a39a83 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/KafkaFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/KafkaFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using Xunit; + [UsedImplicitly] public sealed class KafkaFixture : IAsyncLifetime, IDisposable { private readonly KafkaTestcontainerConfiguration configuration = new KafkaTestcontainerConfiguration(); diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/RabbitMqFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/RabbitMqFixture.cs index cd6f8b336..a0b7627c4 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/RabbitMqFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/Modules/MessageBrokers/RabbitMqFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; + using JetBrains.Annotations; using RabbitMQ.Client; + [UsedImplicitly] public sealed class RabbitMqFixture : DatabaseFixture { private readonly TestcontainerMessageBrokerConfiguration configuration = new RabbitMqTestcontainerConfiguration { Username = "rabbitmq", Password = "rabbitmq" }; diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/NetworkFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/NetworkFixture.cs index f28cb251a..580ac5f16 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/NetworkFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/NetworkFixture.cs @@ -5,8 +5,10 @@ using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Networks; + using JetBrains.Annotations; using Xunit; + [UsedImplicitly] public sealed class NetworkFixture : IAsyncLifetime { public IDockerNetwork Network { get; } diff --git a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/VolumeFixture.cs b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/VolumeFixture.cs index d9605a94e..28ccbaed5 100644 --- a/tests/Testcontainers.Tests/Fixtures/Containers/Unix/VolumeFixture.cs +++ b/tests/Testcontainers.Tests/Fixtures/Containers/Unix/VolumeFixture.cs @@ -5,8 +5,10 @@ namespace DotNet.Testcontainers.Tests.Fixtures using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Volumes; + using JetBrains.Annotations; using Xunit; + [UsedImplicitly] public sealed class VolumeFixture : IAsyncLifetime { private readonly IDockerVolume volume; diff --git a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj index aaa05f53c..7b404d797 100644 --- a/tests/Testcontainers.Tests/Testcontainers.Tests.csproj +++ b/tests/Testcontainers.Tests/Testcontainers.Tests.csproj @@ -1,5 +1,5 @@ - - + + net6.0 false @@ -9,6 +9,7 @@ DotNet.Testcontainers.Tests + @@ -20,13 +21,17 @@ + + + + - + diff --git a/tests/Testcontainers.Tests/Unit/Builders/BuildConfigurationTest.cs b/tests/Testcontainers.Tests/Unit/Builders/BuildConfigurationTest.cs index cee60f6ae..6c6fa8c07 100644 --- a/tests/Testcontainers.Tests/Unit/Builders/BuildConfigurationTest.cs +++ b/tests/Testcontainers.Tests/Unit/Builders/BuildConfigurationTest.cs @@ -57,7 +57,7 @@ public DictionaryCombinationTestData() this.Add(new object[] { null, null, null }); this.Add(new object[] { null, new Dictionary { { "A", "A" } }, new Dictionary { { "A", "A" } } }); this.Add(new object[] { new Dictionary { { "B", "B" } }, null, new Dictionary { { "B", "B" } } }); - this.Add(new object[] { new Dictionary { ["A"] = "new" }, new Dictionary { ["A"] = "old", ["B"] = "B" }, new Dictionary { ["A"] = "new", ["B"] = "B" }, }); + this.Add(new object[] { new Dictionary { ["A"] = "new" }, new Dictionary { ["A"] = "old", ["B"] = "B" }, new Dictionary { ["A"] = "new", ["B"] = "B" } }); } } } diff --git a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs index bd16988b6..e4da39622 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/CustomConfigurationTest.cs @@ -5,7 +5,7 @@ using DotNet.Testcontainers.Configurations; using Xunit; - public sealed class CustomConfigurationTest + public static class CustomConfigurationTest { [CollectionDefinition(nameof(EnvironmentConfigurationTest), DisableParallelization = true)] [Collection(nameof(EnvironmentConfigurationTest))] @@ -15,12 +15,25 @@ public sealed class EnvironmentConfigurationTest : IDisposable static EnvironmentConfigurationTest() { + EnvironmentVariables.Add("DOCKER_CONFIG"); EnvironmentVariables.Add("DOCKER_HOST"); + EnvironmentVariables.Add("DOCKER_AUTH_CONFIG"); EnvironmentVariables.Add("TESTCONTAINERS_RYUK_DISABLED"); EnvironmentVariables.Add("TESTCONTAINERS_RYUK_CONTAINER_IMAGE"); EnvironmentVariables.Add("TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX"); } + [Theory] + [InlineData("", "", null)] + [InlineData("DOCKER_CONFIG", "", null)] + [InlineData("DOCKER_CONFIG", "~/.docker/", "~/.docker/")] + public void GetDockerConfigCustomConfiguration(string propertyName, string propertyValue, string expected) + { + SetEnvironmentVariable(propertyName, propertyValue); + ICustomConfiguration customConfiguration = new EnvironmentConfiguration(); + Assert.Equal(expected, customConfiguration.GetDockerConfig()); + } + [Theory] [InlineData("", "", null)] [InlineData("DOCKER_HOST", "", null)] @@ -32,6 +45,21 @@ public void GetDockerHostCustomConfiguration(string propertyName, string propert Assert.Equal(expected, customConfiguration.GetDockerHost()?.ToString()); } + [Theory] + [InlineData("", "", null)] + [InlineData("DOCKER_AUTH_CONFIG", "", null)] + [InlineData("DOCKER_AUTH_CONFIG", "{jsonReaderException}", null)] + [InlineData("DOCKER_AUTH_CONFIG", "{}", "{}")] + [InlineData("DOCKER_AUTH_CONFIG", "{\"auths\":null}", "{\"auths\":null}")] + [InlineData("DOCKER_AUTH_CONFIG", "{\"auths\":{}}", "{\"auths\":{}}")] + [InlineData("DOCKER_AUTH_CONFIG", "{\"auths\":{\"ghcr.io\":{}}}", "{\"auths\":{\"ghcr.io\":{}}}")] + public void GetDockerAuthConfigCustomConfiguration(string propertyName, string propertyValue, string expected) + { + SetEnvironmentVariable(propertyName, propertyValue); + ICustomConfiguration customConfiguration = new EnvironmentConfiguration(); + Assert.Equal(expected, customConfiguration.GetDockerAuthConfig()?.RootElement.ToString()); + } + [Theory] [InlineData("", "", false)] [InlineData("TESTCONTAINERS_RYUK_DISABLED", "", false)] @@ -85,6 +113,16 @@ private static void SetEnvironmentVariable(string propertyName, string propertyV public sealed class PropertiesFileConfigurationTest { + [Theory] + [InlineData("", null)] + [InlineData("docker.config=", null)] + [InlineData("docker.config=~/.docker/", "~/.docker/")] + public void GetDockerConfigCustomConfiguration(string configuration, string expected) + { + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration }); + Assert.Equal(expected, customConfiguration.GetDockerConfig()); + } + [Theory] [InlineData("", null)] [InlineData("docker.host=", null)] @@ -95,6 +133,20 @@ public void GetDockerHostCustomConfiguration(string configuration, string expect Assert.Equal(expected, customConfiguration.GetDockerHost()?.ToString()); } + [Theory] + [InlineData("", null)] + [InlineData("docker.auth.config=", null)] + [InlineData("docker.auth.config={jsonReaderException}", null)] + [InlineData("docker.auth.config={}", "{}")] + [InlineData("docker.auth.config={\"auths\":null}", "{\"auths\":null}")] + [InlineData("docker.auth.config={\"auths\":{}}", "{\"auths\":{}}")] + [InlineData("docker.auth.config={\"auths\":{\"ghcr.io\":{}}}", "{\"auths\":{\"ghcr.io\":{}}}")] + public void GetDockerAuthConfigCustomConfiguration(string configuration, string expected) + { + ICustomConfiguration customConfiguration = new PropertiesFileConfiguration(new[] { configuration }); + Assert.Equal(expected, customConfiguration.GetDockerAuthConfig()?.RootElement.ToString()); + } + [Theory] [InlineData("", false)] [InlineData("ryuk.disabled=", false)] diff --git a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs index 28fb87550..5f1fe6623 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/DockerEndpointAuthenticationProviderTest.cs @@ -25,9 +25,9 @@ private sealed class AuthConfigTestData : List { public AuthConfigTestData() { - const string DockerHost = "tcp://127.0.0.1:2375"; - Environment.SetEnvironmentVariable("DOCKER_HOST", DockerHost); - this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider().GetAuthConfig(), new Uri(DockerHost) }); + const string dockerHost = "tcp://127.0.0.1:2375"; + Environment.SetEnvironmentVariable("DOCKER_HOST", dockerHost); + this.Add(new object[] { new EnvironmentEndpointAuthenticationProvider().GetAuthConfig(), new Uri(dockerHost) }); this.Add(new object[] { new NpipeEndpointAuthenticationProvider().GetAuthConfig(), new Uri("npipe://./pipe/docker_engine") }); this.Add(new object[] { new UnixEndpointAuthenticationProvider().GetAuthConfig(), new Uri("unix:/var/run/docker.sock") }); Environment.SetEnvironmentVariable("DOCKER_HOST", null); diff --git a/tests/Testcontainers.Tests/Unit/Configurations/TestcontainersAccessInformationTest.cs b/tests/Testcontainers.Tests/Unit/Configurations/TestcontainersAccessInformationTest.cs index c010bee33..bfea3fc6c 100644 --- a/tests/Testcontainers.Tests/Unit/Configurations/TestcontainersAccessInformationTest.cs +++ b/tests/Testcontainers.Tests/Unit/Configurations/TestcontainersAccessInformationTest.cs @@ -18,37 +18,37 @@ public sealed class AccessDockerInformation [Fact] public async Task QueryNotExistingDockerImageById() { - Assert.False(await new DockerImageOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); + Assert.False(await new DockerImageOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); } [Fact] public async Task QueryNotExistingDockerContainerById() { - Assert.False(await new DockerContainerOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); + Assert.False(await new DockerContainerOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); } [Fact] public async Task QueryNotExistingDockerNetworkById() { - Assert.False(await new DockerNetworkOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); + Assert.False(await new DockerNetworkOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithIdAsync(DoesNotExist)); } [Fact] public async Task QueryNotExistingDockerImageByName() { - Assert.False(await new DockerImageOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); + Assert.False(await new DockerImageOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); } [Fact] public async Task QueryNotExistingDockerContainerByName() { - Assert.False(await new DockerContainerOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); + Assert.False(await new DockerContainerOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); } [Fact] public async Task QueryNotExistingDockerNetworkByName() { - Assert.False(await new DockerNetworkOperations(TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); + Assert.False(await new DockerNetworkOperations(Guid.Empty, TestcontainersSettings.OS.DockerEndpointAuthConfig, TestcontainersSettings.Logger).ExistsWithNameAsync(DoesNotExist)); } [Fact] diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/AzuriteTestcontainerTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/AzuriteTestcontainerTest.cs new file mode 100644 index 000000000..7e88081cf --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/AzuriteTestcontainerTest.cs @@ -0,0 +1,262 @@ +namespace DotNet.Testcontainers.Tests.Unit +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Azure; + using Azure.Data.Tables; + using Azure.Storage.Blobs; + using Azure.Storage.Queues; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Tests.Fixtures; + using Xunit; + + public static class AzuriteTestcontainerTest + { + private static readonly string BlobServiceDataFileName = GetDataFileName(AzuriteTestcontainerConfiguration.AzuriteServices.Blob); + + private static readonly string QueueServiceDataFileName = GetDataFileName(AzuriteTestcontainerConfiguration.AzuriteServices.Queue); + + private static readonly string TableServiceDataFileName = GetDataFileName(AzuriteTestcontainerConfiguration.AzuriteServices.Table); + + private static string GetDataFileName(AzuriteTestcontainerConfiguration.AzuriteServices services) + { + return $"__azurite_db_{services.ToString().ToLowerInvariant()}__.json"; + } + + private static bool HasError(Response response) + { + using (var rawResponse = response.GetRawResponse()) + { + return rawResponse.IsError; + } + } + + [Collection(nameof(Testcontainers))] + public sealed class AllServicesEnabled : IClassFixture, IClassFixture + { + private readonly AzuriteFixture.AzuriteDefaultFixture commonContainerPorts; + + private readonly AzuriteFixture.AzuriteDefaultFixture customContainerPorts; + + public AllServicesEnabled(AzuriteFixture.AzuriteDefaultFixture commonContainerPorts, AzuriteFixture.AzuriteWithCustomContainerPortsFixture customContainerPorts) + { + this.commonContainerPorts = commonContainerPorts; + this.customContainerPorts = customContainerPorts; + } + + private AllServicesEnabled(AzuriteFixture.AzuriteDefaultFixture commonContainerPorts) + { + _ = commonContainerPorts; + } + + private AllServicesEnabled(AzuriteFixture.AzuriteWithCustomContainerPortsFixture customContainerPorts) + { + _ = customContainerPorts; + } + + [Fact] + public async Task ShouldEstablishServiceConnection() + { + var exception = await Record.ExceptionAsync(() => Task.WhenAll(EstablishConnection(this.commonContainerPorts), EstablishConnection(this.customContainerPorts))) + .ConfigureAwait(false); + + Assert.Null(exception); + } + + private static async Task EstablishConnection(AzuriteFixture.AzuriteDefaultFixture azurite) + { + // Given + var blobServiceClient = new BlobServiceClient(azurite.Container.ConnectionString); + + var queueServiceClient = new QueueServiceClient(azurite.Container.ConnectionString); + + var tableServiceClient = new TableServiceClient(azurite.Container.ConnectionString); + + // When + var blobProperties = await blobServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var queueProperties = await queueServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var tableProperties = await tableServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var execResult = await azurite.Container.ExecAsync(new List { "ls", AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath }) + .ConfigureAwait(false); + + // Then + Assert.False(HasError(blobProperties)); + Assert.False(HasError(queueProperties)); + Assert.False(HasError(tableProperties)); + Assert.Equal(0, execResult.ExitCode); + Assert.Equal(azurite.Configuration.BlobContainerPort, azurite.Container.ContainerBlobPort); + Assert.Equal(azurite.Configuration.QueueContainerPort, azurite.Container.ContainerQueuePort); + Assert.Equal(azurite.Configuration.TableContainerPort, azurite.Container.ContainerTablePort); + Assert.Contains(BlobServiceDataFileName, execResult.Stdout); + Assert.Contains(QueueServiceDataFileName, execResult.Stdout); + Assert.Contains(TableServiceDataFileName, execResult.Stdout); + } + } + + [Collection(nameof(Testcontainers))] + public sealed class BlobServiceEnabled : IClassFixture + { + private readonly AzuriteFixture.AzuriteDefaultFixture azurite; + + public BlobServiceEnabled(AzuriteFixture.AzuriteWithBlobOnlyFixture azurite) + { + this.azurite = azurite; + } + + [Fact] + public async Task ShouldEstablishServiceConnection() + { + // Given + var blobServiceClient = new BlobServiceClient(this.azurite.Container.ConnectionString); + + var queueServiceClient = new QueueServiceClient(this.azurite.Container.ConnectionString); + + var tableServiceClient = new TableServiceClient(this.azurite.Container.ConnectionString); + + // When + var blobProperties = await blobServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var execResult = await this.azurite.Container.ExecAsync(new List { "ls", AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath }) + .ConfigureAwait(false); + + // Then + Assert.False(HasError(blobProperties)); + Assert.Equal(0, execResult.ExitCode); + Assert.Contains(BlobServiceDataFileName, execResult.Stdout); + + Assert.DoesNotContain(QueueServiceDataFileName, execResult.Stdout); + Assert.DoesNotContain(TableServiceDataFileName, execResult.Stdout); + + await Assert.ThrowsAsync(() => queueServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + + await Assert.ThrowsAsync(() => tableServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + } + } + + [Collection(nameof(Testcontainers))] + public sealed class QueueServiceEnabled : IClassFixture + { + private readonly AzuriteFixture.AzuriteDefaultFixture azurite; + + public QueueServiceEnabled(AzuriteFixture.AzuriteWithQueueOnlyFixture azurite) + { + this.azurite = azurite; + } + + [Fact] + public async Task ShouldEstablishServiceConnection() + { + // Given + var blobServiceClient = new BlobServiceClient(this.azurite.Container.ConnectionString); + + var queueServiceClient = new QueueServiceClient(this.azurite.Container.ConnectionString); + + var tableServiceClient = new TableServiceClient(this.azurite.Container.ConnectionString); + + // When + var queueProperties = await queueServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var execResult = await this.azurite.Container.ExecAsync(new List { "ls", AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath }) + .ConfigureAwait(false); + + // Then + Assert.False(HasError(queueProperties)); + Assert.Equal(0, execResult.ExitCode); + Assert.Contains(QueueServiceDataFileName, execResult.Stdout); + + Assert.DoesNotContain(BlobServiceDataFileName, execResult.Stdout); + Assert.DoesNotContain(TableServiceDataFileName, execResult.Stdout); + + await Assert.ThrowsAsync(() => blobServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + + await Assert.ThrowsAsync(() => tableServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + } + } + + [Collection(nameof(Testcontainers))] + public sealed class TableServiceEnabled : IClassFixture + { + private readonly AzuriteFixture.AzuriteDefaultFixture azurite; + + public TableServiceEnabled(AzuriteFixture.AzuriteWithTableOnlyFixture azurite) + { + this.azurite = azurite; + } + + [Fact] + public async Task ShouldEstablishServiceConnection() + { + // Given + var blobServiceClient = new BlobServiceClient(this.azurite.Container.ConnectionString); + + var queueServiceClient = new QueueServiceClient(this.azurite.Container.ConnectionString); + + var tableServiceClient = new TableServiceClient(this.azurite.Container.ConnectionString); + + // When + var tableProperties = await tableServiceClient.GetPropertiesAsync() + .ConfigureAwait(false); + + var execResult = await this.azurite.Container.ExecAsync(new List { "ls", AzuriteTestcontainerConfiguration.DefaultWorkspaceDirectoryPath }) + .ConfigureAwait(false); + + // Then + Assert.False(HasError(tableProperties)); + Assert.Equal(0, execResult.ExitCode); + Assert.Contains(TableServiceDataFileName, execResult.Stdout); + + Assert.DoesNotContain(BlobServiceDataFileName, execResult.Stdout); + Assert.DoesNotContain(QueueServiceDataFileName, execResult.Stdout); + + await Assert.ThrowsAsync(() => blobServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + + await Assert.ThrowsAsync(() => queueServiceClient.GetPropertiesAsync()) + .ConfigureAwait(false); + } + } + + [Collection(nameof(Testcontainers))] + public sealed class CustomLocation : IClassFixture + { + private readonly IEnumerable dataFiles; + + public CustomLocation(AzuriteFixture.AzuriteWithCustomWorkspaceFixture azurite) + { + if (Directory.Exists(azurite.Configuration.Location)) + { + this.dataFiles = Directory.EnumerateFiles(azurite.Configuration.Location, "*", SearchOption.TopDirectoryOnly) + .Select(Path.GetFileName) + .ToArray(); + } + else + { + this.dataFiles = Array.Empty(); + } + } + + [Fact] + public void ShouldGetDataFiles() + { + Assert.Contains(BlobServiceDataFileName, this.dataFiles); + Assert.Contains(QueueServiceDataFileName, this.dataFiles); + Assert.Contains(TableServiceDataFileName, this.dataFiles); + } + } + } +} diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/ElasticsearchTestcontainerTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/ElasticsearchTestcontainerTest.cs new file mode 100644 index 000000000..e818267bf --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/ElasticsearchTestcontainerTest.cs @@ -0,0 +1,48 @@ +namespace DotNet.Testcontainers.Tests.Unit +{ + using System; + using System.Threading.Tasks; + using DotNet.Testcontainers.Configurations; + using DotNet.Testcontainers.Tests.Fixtures; + using Xunit; + + [Collection(nameof(Testcontainers))] + public sealed class ElasticsearchTestcontainerTest : IClassFixture + { + private readonly ElasticsearchFixture elasticsearchFixture; + + public ElasticsearchTestcontainerTest(ElasticsearchFixture elasticsearchFixture) + { + this.elasticsearchFixture = elasticsearchFixture; + } + + [Fact] + [Trait("Category", "Elasticsearch")] + public async Task ConnectionEstablished() + { + // Given + var connection = this.elasticsearchFixture.Connection; + + // When + var result = await connection.InfoAsync() + .ConfigureAwait(false); + + // Then + Assert.True(result.IsValid); + } + + [Fact] + public void CannotSetDatabase() + { + var elasticsearch = new ElasticsearchTestcontainerConfiguration(); + Assert.Throws(() => elasticsearch.Database = string.Empty); + } + + [Fact] + public void CannotSetUsername() + { + var elasticsearch = new ElasticsearchTestcontainerConfiguration(); + Assert.Throws(() => elasticsearch.Username = string.Empty); + } + } +} diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MariaDbTestcontainerTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MariaDbTestcontainerTest.cs new file mode 100644 index 000000000..f45fbf76d --- /dev/null +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MariaDbTestcontainerTest.cs @@ -0,0 +1,69 @@ +namespace DotNet.Testcontainers.Tests.Unit +{ + using System.Data; + using System.Threading.Tasks; + using DotNet.Testcontainers.Tests.Fixtures; + using Xunit; + + [Collection(nameof(Testcontainers))] + public sealed class MariaDbTestcontainerTest : IClassFixture + { + private readonly MariaDbFixture mariaDbFixture; + + public MariaDbTestcontainerTest(MariaDbFixture mariaDbFixture) + { + this.mariaDbFixture = mariaDbFixture; + } + + [Fact] + public async Task ConnectionEstablished() + { + // Given + var connection = this.mariaDbFixture.Connection; + + // When + await connection.OpenAsync() + .ConfigureAwait(false); + + // Then + Assert.Equal(ConnectionState.Open, connection.State); + } + + [Fact] + public async Task ExecScriptInRunningContainer() + { + // Given + const string script = @" + CREATE TABLE MyTable ( + id INT(6) UNSIGNED PRIMARY KEY, + name VARCHAR(30) NOT NULL + ); + INSERT INTO MyTable (id, name) VALUES (1, 'MyName'); + SELECT * FROM MyTable; + "; + + // When + var result = await this.mariaDbFixture.Container.ExecScriptAsync(script) + .ConfigureAwait(false); + + // Then + Assert.Equal(0, result.ExitCode); + Assert.Contains("MyName", result.Stdout); + } + + [Fact] + public async Task ThrowErrorInRunningContainerWithInvalidScript() + { + // Given + const string script = "invalid SQL command"; + + // When + var result = await this.mariaDbFixture.Container.ExecScriptAsync(script) + .ConfigureAwait(false); + + // Then + Assert.Equal(0, result.ExitCode); // exit code is 0 because MariaDB docker image does not have a proper error handler + Assert.Contains("ERROR 1064 (42000)", result.Stderr); + } + } +} diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MongoDbTestcontainerTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MongoDbTestcontainerTest.cs index 92a315ed9..24cbf7323 100644 --- a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MongoDbTestcontainerTest.cs +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MongoDbTestcontainerTest.cs @@ -6,13 +6,26 @@ using Xunit; [Collection(nameof(Testcontainers))] - public sealed class MongoDbTestcontainerTest : IClassFixture + public sealed class MongoDbTestcontainerTest : IClassFixture, IClassFixture { private readonly MongoDbFixture mongoDbFixture; - public MongoDbTestcontainerTest(MongoDbFixture mongoDbFixture) + private readonly MongoDbNoAuthFixture mongoDbNoAuthFixture; + + public MongoDbTestcontainerTest(MongoDbFixture mongoDbFixture, MongoDbNoAuthFixture mongoDbNoAuthFixture) { this.mongoDbFixture = mongoDbFixture; + this.mongoDbNoAuthFixture = mongoDbNoAuthFixture; + } + + private MongoDbTestcontainerTest(MongoDbFixture mongoDbFixture) + { + _ = mongoDbFixture; + } + + private MongoDbTestcontainerTest(MongoDbNoAuthFixture mongoDbNoAuthFixture) + { + _ = mongoDbNoAuthFixture; } [Fact] @@ -22,11 +35,23 @@ public async Task ConnectionEstablished() var connection = this.mongoDbFixture.Connection; // When - var result = await connection.RunCommandAsync(@"{ ping: 1 }") + var result = await connection.RunCommandAsync("{ ping: 1 }") .ConfigureAwait(false); // Then Assert.Equal(1.0, result["ok"].AsDouble); } + + [Fact] + public void ConnectionStringShouldContainAuthInformation() + { + Assert.Matches("mongodb:\\/\\/\\w+:\\w+@\\w+:\\d+", this.mongoDbFixture.Container.ConnectionString); + } + + [Fact] + public void ConnectionStringShouldNotContainAuthInformation() + { + Assert.Matches("mongodb:\\/\\/\\w+:\\d+", this.mongoDbNoAuthFixture.Container.ConnectionString); + } } } diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MsSqlTestcontainerTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MsSqlTestcontainerTest.cs index 3a0a4e534..1f0a55bf8 100644 --- a/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MsSqlTestcontainerTest.cs +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/Modules/Databases/MsSqlTestcontainerTest.cs @@ -31,13 +31,6 @@ await connection.OpenAsync() Assert.Equal(ConnectionState.Open, connection.State); } - [Fact] - public void CannotSetDatabase() - { - var mssql = new MsSqlTestcontainerConfiguration(); - Assert.Throws(() => mssql.Database = string.Empty); - } - [Fact] public void CannotSetUsername() { diff --git a/tests/Testcontainers.Tests/Unit/Containers/Unix/ResourceReaperCancellableTest.cs b/tests/Testcontainers.Tests/Unit/Containers/Unix/ResourceReaperCancellableTest.cs index 57fe42bb6..fc843bed2 100644 --- a/tests/Testcontainers.Tests/Unit/Containers/Unix/ResourceReaperCancellableTest.cs +++ b/tests/Testcontainers.Tests/Unit/Containers/Unix/ResourceReaperCancellableTest.cs @@ -41,7 +41,7 @@ await resourceReaper.DisposeAsync() [Fact] public async Task ResourceReaperShouldTimeoutIfInitializationFails() { - var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx"); + var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx", TimeSpan.FromSeconds(10)); _ = await Assert.ThrowsAsync(() => resourceReaperTask); Assert.Equal(new[] { ResourceReaperState.Created, ResourceReaperState.InitializingConnection }, this.stateChanges); } @@ -51,7 +51,7 @@ public async Task GetAndStartNewAsyncShouldBeCancellableDuringContainerStart() { ResourceReaper.StateChanged += this.CancelOnCreated; - var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx", TimeSpan.FromSeconds(60), this.cts.Token); + var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx", TimeSpan.FromSeconds(10), this.cts.Token); _ = await Assert.ThrowsAnyAsync(() => resourceReaperTask); Assert.Equal(new[] { ResourceReaperState.Created }, this.stateChanges); } @@ -61,7 +61,7 @@ public async Task GetAndStartNewAsyncShouldBeCancellableDuringInitializingConnec { ResourceReaper.StateChanged += this.CancelOnInitializingConnection; - var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx", TimeSpan.FromSeconds(60), this.cts.Token); + var resourceReaperTask = ResourceReaper.GetAndStartNewAsync(this.sessionId, null, "nginx", TimeSpan.FromSeconds(10), this.cts.Token); _ = await Assert.ThrowsAsync(() => resourceReaperTask); Assert.Equal(new[] { ResourceReaperState.Created, ResourceReaperState.InitializingConnection }, this.stateChanges); } diff --git a/tests/Testcontainers.Tests/Unit/GuardTest.cs b/tests/Testcontainers.Tests/Unit/GuardTest.cs index 4ec73c57e..1b3ff70e4 100644 --- a/tests/Testcontainers.Tests/Unit/GuardTest.cs +++ b/tests/Testcontainers.Tests/Unit/GuardTest.cs @@ -1,6 +1,7 @@ namespace DotNet.Testcontainers.Tests.Unit { using System; + using DotNet.Testcontainers.Configurations; using Xunit; public static class GuardTest @@ -22,15 +23,28 @@ public void IfNotNull() var exception = Record.Exception(() => Guard.Argument(new object(), nameof(this.IfNotNull)).NotNull()); Assert.Null(exception); } + + [Fact] + public void IfDockerEndpointAuthConfigIsSet() + { + var exception = Record.Exception(() => Guard.Argument(new DockerEndpointAuthenticationConfiguration(new Uri("tcp://127.0.0.1:2375")), nameof(this.IfDockerEndpointAuthConfigIsSet)).DockerEndpointAuthConfigIsSet()); + Assert.Null(exception); + } } - public sealed class ThrowArgumentNullExceptionMatchImage + public sealed class ThrowArgumentNullException { [Fact] public void IfNull() { Assert.Throws(() => Guard.Argument((object)null, nameof(this.IfNull)).NotNull()); } + + [Fact] + public void IfDockerEndpointAuthConfigIsSet() + { + Assert.Throws(() => Guard.Argument((IDockerEndpointAuthenticationConfiguration)null, nameof(this.IfDockerEndpointAuthConfigIsSet)).DockerEndpointAuthConfigIsSet()); + } } public sealed class ThrowArgumentException