diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e12f947b2..129fe97c4 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -50,6 +50,7 @@ jobs: { name: "Testcontainers.Couchbase", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.CouchDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Db2", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.DragonflyDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.DynamoDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Elasticsearch", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.EventHubs", runs-on: "ubuntu-22.04" }, diff --git a/Testcontainers.sln b/Testcontainers.sln index e09e2b093..404dd09ba 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -257,6 +257,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit.Tests" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.XunitV3.Tests", "tests\Testcontainers.XunitV3.Tests\Testcontainers.XunitV3.Tests.csproj", "{B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DragonflyDb", "src\Testcontainers.DragonflyDb\Testcontainers.DragonflyDb.csproj", "{0686A718-3933-D826-BDC5-2F683AB593CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.DragonflyDb.Tests", "tests\Testcontainers.DragonflyDb.Tests\Testcontainers.DragonflyDb.Tests.csproj", "{546824C2-F360-5F78-1FC6-3D1E191C3FAF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -751,6 +755,14 @@ Global {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB}.Release|Any CPU.Build.0 = Release|Any CPU + {0686A718-3933-D826-BDC5-2F683AB593CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0686A718-3933-D826-BDC5-2F683AB593CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0686A718-3933-D826-BDC5-2F683AB593CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0686A718-3933-D826-BDC5-2F683AB593CD}.Release|Any CPU.Build.0 = Release|Any CPU + {546824C2-F360-5F78-1FC6-3D1E191C3FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {546824C2-F360-5F78-1FC6-3D1E191C3FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {546824C2-F360-5F78-1FC6-3D1E191C3FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {546824C2-F360-5F78-1FC6-3D1E191C3FAF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -878,5 +890,10 @@ Global {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {B2E8B7FB-7D1E-4DD3-A25E-34DE4386B1EB} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {0686A718-3933-D826-BDC5-2F683AB593CD} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {546824C2-F360-5F78-1FC6-3D1E191C3FAF} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3BA67751-0C2A-4D17-A84E-C0AC59B94254} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.DragonflyDb/.editorconfig b/src/Testcontainers.DragonflyDb/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.DragonflyDb/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.DragonflyDb/DragonflyDbBuilder.cs b/src/Testcontainers.DragonflyDb/DragonflyDbBuilder.cs new file mode 100644 index 000000000..6da4a80ff --- /dev/null +++ b/src/Testcontainers.DragonflyDb/DragonflyDbBuilder.cs @@ -0,0 +1,66 @@ +namespace Testcontainers.DragonflyDb; + +/// +[PublicAPI] +public sealed class DragonflyDbBuilder : ContainerBuilder +{ + public const string DragonflyDbImage = "docker.dragonflydb.io/dragonflydb/dragonfly"; + + public const ushort DragonflyDbPort = 6379; + + /// + /// Initializes a new instance of the class. + /// + public DragonflyDbBuilder() + : this(new DragonflyDbConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private DragonflyDbBuilder(DragonflyDbConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override DragonflyDbConfiguration DockerResourceConfiguration { get; } + + /// + public override DragonflyDbContainer Build() + { + Validate(); + return new DragonflyDbContainer(DockerResourceConfiguration); + } + + /// + protected override DragonflyDbBuilder Init() + { + return base.Init() + .WithImage(DragonflyDbImage) + .WithPortBinding(DragonflyDbPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("/usr/local/bin/healthcheck.sh")); + } + + /// + protected override DragonflyDbBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new DragonflyDbConfiguration(resourceConfiguration)); + } + + /// + protected override DragonflyDbBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new DragonflyDbConfiguration(resourceConfiguration)); + } + + /// + protected override DragonflyDbBuilder Merge(DragonflyDbConfiguration oldValue, DragonflyDbConfiguration newValue) + { + return new DragonflyDbBuilder(new DragonflyDbConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.DragonflyDb/DragonflyDbConfiguration.cs b/src/Testcontainers.DragonflyDb/DragonflyDbConfiguration.cs new file mode 100644 index 000000000..2b8918876 --- /dev/null +++ b/src/Testcontainers.DragonflyDb/DragonflyDbConfiguration.cs @@ -0,0 +1,53 @@ +namespace Testcontainers.DragonflyDb; + +/// +[PublicAPI] +public sealed class DragonflyDbConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public DragonflyDbConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public DragonflyDbConfiguration(IResourceConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public DragonflyDbConfiguration(IContainerConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public DragonflyDbConfiguration(DragonflyDbConfiguration resourceConfiguration) + : this(new DragonflyDbConfiguration(), resourceConfiguration) + { + // Passes the configuration upwards to the base implementations to create an updated immutable copy. + } + + /// + /// Initializes a new instance of the class. + /// + /// The old Docker resource configuration. + /// The new Docker resource configuration. + public DragonflyDbConfiguration(DragonflyDbConfiguration oldValue, DragonflyDbConfiguration newValue) + : base(oldValue, newValue) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.DragonflyDb/DragonflyDbContainer.cs b/src/Testcontainers.DragonflyDb/DragonflyDbContainer.cs new file mode 100644 index 000000000..0ff6bd343 --- /dev/null +++ b/src/Testcontainers.DragonflyDb/DragonflyDbContainer.cs @@ -0,0 +1,42 @@ +namespace Testcontainers.DragonflyDb; + +/// +[PublicAPI] +public sealed class DragonflyDbContainer : DockerContainer +{ + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public DragonflyDbContainer(DragonflyDbConfiguration configuration) + : base(configuration) + { + } + + /// + /// Gets the DragonflyDb connection string. + /// + /// The DragonflyDb connection string. + public string GetConnectionString() + { + return new UriBuilder("DragonflyDb", Hostname, GetMappedPublicPort(DragonflyDbBuilder.DragonflyDbPort)).Uri.Authority; + } + + /// + /// Executes the Lua script in the DragonflyDb container. + /// + /// The content of the Lua script to execute. + /// Cancellation token. + /// Task that completes when the Lua script has been executed. + public async Task ExecScriptAsync(string scriptContent, CancellationToken ct = default) + { + var scriptFilePath = string.Join("/", string.Empty, "tmp", Guid.NewGuid().ToString("D"), Path.GetRandomFileName()); + + await CopyAsync(Encoding.Default.GetBytes(scriptContent), scriptFilePath, fileMode: Unix.FileMode644, ct: ct) + .ConfigureAwait(false); + + //DragonflyDb uses the same cli as redis + return await ExecAsync(new[] { "redis-cli", "--eval", scriptFilePath, "0" }, ct) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Testcontainers.DragonflyDb/Testcontainers.DragonflyDb.csproj b/src/Testcontainers.DragonflyDb/Testcontainers.DragonflyDb.csproj new file mode 100644 index 000000000..9a25b9c4d --- /dev/null +++ b/src/Testcontainers.DragonflyDb/Testcontainers.DragonflyDb.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.DragonflyDb/Usings.cs b/src/Testcontainers.DragonflyDb/Usings.cs new file mode 100644 index 000000000..3c22a4545 --- /dev/null +++ b/src/Testcontainers.DragonflyDb/Usings.cs @@ -0,0 +1,10 @@ +global using System; +global using System.IO; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; \ No newline at end of file diff --git a/tests/Testcontainers.DragonflyDb.Tests/.editorconfig b/tests/Testcontainers.DragonflyDb.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.DragonflyDb.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.DragonflyDb.Tests/DragonflyDbContainerTest.cs b/tests/Testcontainers.DragonflyDb.Tests/DragonflyDbContainerTest.cs new file mode 100644 index 000000000..22a8928af --- /dev/null +++ b/tests/Testcontainers.DragonflyDb.Tests/DragonflyDbContainerTest.cs @@ -0,0 +1,60 @@ +namespace Testcontainers.DragonflyDb; + +public sealed class DragonflyDbContainerTest : IAsyncLifetime +{ + private readonly DragonflyDbContainer _dragonflyDbContainer = new DragonflyDbBuilder().Build(); + + public async ValueTask InitializeAsync() + { + await _dragonflyDbContainer.StartAsync() + .ConfigureAwait(false); + } + + public ValueTask DisposeAsync() + { + return _dragonflyDbContainer.DisposeAsync(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ConnectionStateReturnsOpen() + { + using var connection = ConnectionMultiplexer.Connect(_dragonflyDbContainer.GetConnectionString()); + Assert.True(connection.IsConnected); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ExecScriptReturnsSuccessful() + { + // Given + const string scriptContent = "return 'Hello, scripting!'"; + + // When + var execResult = await _dragonflyDbContainer.ExecScriptAsync(scriptContent, TestContext.Current.CancellationToken) + .ConfigureAwait(true); + + // Then + Assert.Equal(0L, execResult.ExitCode); + Assert.Equal("Hello, scripting!\n", execResult.Stdout); + Assert.Empty(execResult.Stderr); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task SetAndGetValueReturnsExpected() + { + // Given + using var connection = ConnectionMultiplexer.Connect(_dragonflyDbContainer.GetConnectionString()); + var database = connection.GetDatabase(); + const string key = "test-key"; + const string value = "test-value"; + + // When + await database.StringSetAsync(key, value); + var result = await database.StringGetAsync(key); + + // Then + Assert.Equal(value, result); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.DragonflyDb.Tests/Testcontainers.DragonflyDb.Tests.csproj b/tests/Testcontainers.DragonflyDb.Tests/Testcontainers.DragonflyDb.Tests.csproj new file mode 100644 index 000000000..f6412ac5e --- /dev/null +++ b/tests/Testcontainers.DragonflyDb.Tests/Testcontainers.DragonflyDb.Tests.csproj @@ -0,0 +1,19 @@ + + + net9.0 + false + false + Exe + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.DragonflyDb.Tests/Usings.cs b/tests/Testcontainers.DragonflyDb.Tests/Usings.cs new file mode 100644 index 000000000..4e7182108 --- /dev/null +++ b/tests/Testcontainers.DragonflyDb.Tests/Usings.cs @@ -0,0 +1,4 @@ +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using StackExchange.Redis; +global using Xunit; \ No newline at end of file