diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index e1f122658..a418cf98b 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -42,6 +42,7 @@ jobs: { name: "Testcontainers.Azurite", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.BigQuery", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Bigtable", runs-on: "ubuntu-22.04" }, + { name: "Testcontainers.Cassandra", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.ClickHouse", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.CockroachDb", runs-on: "ubuntu-22.04" }, { name: "Testcontainers.Consul", runs-on: "ubuntu-22.04" }, diff --git a/Directory.Packages.props b/Directory.Packages.props index 0a4db1cff..4c8f9839c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/Testcontainers.sln b/Testcontainers.sln index 96eb3545f..1bac70646 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -23,6 +23,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Bigtable", "src\Testcontainers.Bigtable\Testcontainers.Bigtable.csproj", "{302EC1E0-AE75-4E99-A6BF-524F35338BC8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Cassandra", "src\Testcontainers.Cassandra\Testcontainers.Cassandra.csproj", "{8495D757-5FD7-491C-B941-9D43B3DCF3C0}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ClickHouse", "src\Testcontainers.ClickHouse\Testcontainers.ClickHouse.csproj", "{B061A78E-536E-4CA1-8401-234D5FBFBAB7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.CockroachDb", "src\Testcontainers.CockroachDb\Testcontainers.CockroachDb.csproj", "{8D9871C6-5A39-4F0B-A15A-E87D34F3EA73}" @@ -123,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.BigQuery.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Bigtable.Tests", "tests\Testcontainers.Bigtable.Tests\Testcontainers.Bigtable.Tests.csproj", "{2E7B92E3-8526-4706-90F3-00F0F5C47C37}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Cassandra.Tests", "tests\Testcontainers.Cassandra.Tests\Testcontainers.Cassandra.Tests.csproj", "{C6A2B99E-BFD5-4510-83D7-A8844142F27D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ClickHouse.Tests", "tests\Testcontainers.ClickHouse.Tests\Testcontainers.ClickHouse.Tests.csproj", "{9D0A0B32-4921-400C-99CB-8650677E3E44}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.CockroachDb.Tests", "tests\Testcontainers.CockroachDb.Tests\Testcontainers.CockroachDb.Tests.csproj", "{685E6D9A-B05E-41D9-A08E-5F3CA7733F7D}" @@ -250,6 +254,10 @@ Global {302EC1E0-AE75-4E99-A6BF-524F35338BC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {302EC1E0-AE75-4E99-A6BF-524F35338BC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {302EC1E0-AE75-4E99-A6BF-524F35338BC8}.Release|Any CPU.Build.0 = Release|Any CPU + {8495D757-5FD7-491C-B941-9D43B3DCF3C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8495D757-5FD7-491C-B941-9D43B3DCF3C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8495D757-5FD7-491C-B941-9D43B3DCF3C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8495D757-5FD7-491C-B941-9D43B3DCF3C0}.Release|Any CPU.Build.0 = Release|Any CPU {B061A78E-536E-4CA1-8401-234D5FBFBAB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B061A78E-536E-4CA1-8401-234D5FBFBAB7}.Debug|Any CPU.Build.0 = Debug|Any CPU {B061A78E-536E-4CA1-8401-234D5FBFBAB7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -450,6 +458,10 @@ Global {2E7B92E3-8526-4706-90F3-00F0F5C47C37}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E7B92E3-8526-4706-90F3-00F0F5C47C37}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E7B92E3-8526-4706-90F3-00F0F5C47C37}.Release|Any CPU.Build.0 = Release|Any CPU + {C6A2B99E-BFD5-4510-83D7-A8844142F27D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A2B99E-BFD5-4510-83D7-A8844142F27D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6A2B99E-BFD5-4510-83D7-A8844142F27D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6A2B99E-BFD5-4510-83D7-A8844142F27D}.Release|Any CPU.Build.0 = Release|Any CPU {9D0A0B32-4921-400C-99CB-8650677E3E44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D0A0B32-4921-400C-99CB-8650677E3E44}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D0A0B32-4921-400C-99CB-8650677E3E44}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -653,6 +665,7 @@ Global {3F2E254F-C203-43FD-A078-DC3E2CBC0F9F} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {A9FF9C7F-BBA0-4B44-90B7-48A60F9E00F3} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {302EC1E0-AE75-4E99-A6BF-524F35338BC8} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {8495D757-5FD7-491C-B941-9D43B3DCF3C0} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {B061A78E-536E-4CA1-8401-234D5FBFBAB7} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8D9871C6-5A39-4F0B-A15A-E87D34F3EA73} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {51ED33B9-B688-401E-85F2-329D3C935BD1} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -703,6 +716,7 @@ Global {B272FDDE-5E01-425D-B9E1-10FF883DDAAA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {03E60673-078A-4508-99AD-8537CE6F78F1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {2E7B92E3-8526-4706-90F3-00F0F5C47C37} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {C6A2B99E-BFD5-4510-83D7-A8844142F27D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {9D0A0B32-4921-400C-99CB-8650677E3E44} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {685E6D9A-B05E-41D9-A08E-5F3CA7733F7D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {2478673C-B063-469D-ABD1-0C3E0A25541B} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/docs/modules/index.md b/docs/modules/index.md index 6e6681edb..449deb312 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -29,6 +29,7 @@ await moduleNameContainer.StartAsync(); | Azurite | `mcr.microsoft.com/azure-storage/azurite:3.24.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Azurite) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Azurite) | | BigQuery | `ghcr.io/goccy/bigquery-emulator:0.4` | [NuGet](https://www.nuget.org/packages/Testcontainers.BigQuery) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.BigQuery) | | Bigtable | `gcr.io/google.com/cloudsdktool/google-cloud-cli:446.0.1-emulators` | [NuGet](https://www.nuget.org/packages/Testcontainers.Bigtable) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Bigtable) | +| Cassandra | `library/cassandra:5.0` | [NuGet](https://www.nuget.org/packages/Testcontainers.Cassandra) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Cassandra) | | ClickHouse | `clickhouse/clickhouse-server:23.6-alpine` | [NuGet](https://www.nuget.org/packages/Testcontainers.ClickHouse) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.ClickHouse) | | CockroachDB | `cockroachdb:23.1.13` | [NuGet](https://www.nuget.org/packages/Testcontainers.CockroachDb) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.CockroachDb) | | Consul | `consul:1.15` | [NuGet](https://www.nuget.org/packages/Testcontainers.Consul) | [Source](https://github.com/testcontainers/testcontainers-dotnet/tree/develop/src/Testcontainers.Consul) | diff --git a/src/Testcontainers.Cassandra/.editorconfig b/src/Testcontainers.Cassandra/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.Cassandra/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.Cassandra/CassandraBuilder.cs b/src/Testcontainers.Cassandra/CassandraBuilder.cs new file mode 100644 index 000000000..61a6f82b4 --- /dev/null +++ b/src/Testcontainers.Cassandra/CassandraBuilder.cs @@ -0,0 +1,74 @@ +namespace Testcontainers.Cassandra; + +/// +[PublicAPI] +public sealed class CassandraBuilder : ContainerBuilder +{ + public const string CassandraImage = "cassandra:5.0"; + + public const ushort CqlPort = 9042; + + public const string DefaultDatacenterName = "dc1"; + + /// + /// Initializes a new instance of the class. + /// + public CassandraBuilder() + : this(new CassandraConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private CassandraBuilder(CassandraConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override CassandraConfiguration DockerResourceConfiguration { get; } + + /// + public override CassandraContainer Build() + { + Validate(); + return new CassandraContainer(DockerResourceConfiguration); + } + + /// + protected override CassandraBuilder Init() + { + return base.Init() + .WithImage(CassandraImage) + .WithPortBinding(CqlPort, true) + .WithEnvironment("JVM_OPTS", "-Dcassandra.skip_wait_for_gossip_to_settle=0 -Dcassandra.initial_token=0") + .WithEnvironment("HEAP_NEWSIZE", "128M") + .WithEnvironment("MAX_HEAP_SIZE", "1024M") + .WithEnvironment("CASSANDRA_SNITCH", "GossipingPropertyFileSnitch") + .WithEnvironment("CASSANDRA_ENDPOINT_SNITCH", "GossipingPropertyFileSnitch") + .WithEnvironment("CASSANDRA_DC", DefaultDatacenterName) + .WithWaitStrategy(Wait.ForUnixContainer().UntilMessageIsLogged("Startup complete")); + } + + /// + protected override CassandraBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new CassandraConfiguration(resourceConfiguration)); + } + + /// + protected override CassandraBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new CassandraConfiguration(resourceConfiguration)); + } + + /// + protected override CassandraBuilder Merge(CassandraConfiguration oldValue, CassandraConfiguration newValue) + { + return new CassandraBuilder(new CassandraConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Cassandra/CassandraConfiguration.cs b/src/Testcontainers.Cassandra/CassandraConfiguration.cs new file mode 100644 index 000000000..a1a02c62b --- /dev/null +++ b/src/Testcontainers.Cassandra/CassandraConfiguration.cs @@ -0,0 +1,53 @@ +namespace Testcontainers.Cassandra; + +/// +[PublicAPI] +public sealed class CassandraConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + public CassandraConfiguration() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public CassandraConfiguration(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 CassandraConfiguration(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 CassandraConfiguration(CassandraConfiguration resourceConfiguration) + : this(new CassandraConfiguration(), 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 CassandraConfiguration(CassandraConfiguration oldValue, CassandraConfiguration newValue) + : base(oldValue, newValue) + { + } +} \ No newline at end of file diff --git a/src/Testcontainers.Cassandra/CassandraContainer.cs b/src/Testcontainers.Cassandra/CassandraContainer.cs new file mode 100644 index 000000000..d9fcd7557 --- /dev/null +++ b/src/Testcontainers.Cassandra/CassandraContainer.cs @@ -0,0 +1,41 @@ +namespace Testcontainers.Cassandra; + +/// +[PublicAPI] +public sealed class CassandraContainer : DockerContainer, IDatabaseContainer +{ + /// + public CassandraContainer(CassandraConfiguration configuration) + : base(configuration) + { + } + + /// + /// Gets the Cassandra connection string. + /// + /// The Cassandra connection string. + public string GetConnectionString() + { + var properties = new Dictionary(); + properties.Add("Contact Points", Hostname); + properties.Add("Port", GetMappedPublicPort(CassandraBuilder.CqlPort).ToString()); + return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); + } + + /// + /// Executes the CQL script in the Cassandra container. + /// + /// The content of the CQL script to execute. + /// Cancellation token. + /// Task that completes when the CQL 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, Unix.FileMode644, ct) + .ConfigureAwait(false); + + return await ExecAsync(new[] { "cqlsh", "--file", scriptFilePath }, ct) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Testcontainers.Cassandra/Testcontainers.Cassandra.csproj b/src/Testcontainers.Cassandra/Testcontainers.Cassandra.csproj new file mode 100644 index 000000000..906f34018 --- /dev/null +++ b/src/Testcontainers.Cassandra/Testcontainers.Cassandra.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.Cassandra/Usings.cs b/src/Testcontainers.Cassandra/Usings.cs new file mode 100644 index 000000000..191eb3bce --- /dev/null +++ b/src/Testcontainers.Cassandra/Usings.cs @@ -0,0 +1,12 @@ +global using System; +global using System.IO; +global using System.Text; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using System.Collections.Generic; +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.Cassandra.Tests/.editorconfig b/tests/Testcontainers.Cassandra.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.Cassandra.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs b/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs new file mode 100644 index 000000000..0af198beb --- /dev/null +++ b/tests/Testcontainers.Cassandra.Tests/CassandraContainerTest.cs @@ -0,0 +1,69 @@ +namespace Testcontainers.Cassandra; + +public sealed class CassandraContainerTest : IAsyncLifetime +{ + private readonly CassandraContainer _cassandraContainer = new CassandraBuilder().Build(); + + public Task InitializeAsync() + { + return _cassandraContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _cassandraContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ConnectionStateReturnsOpen() + { + // Given + using DbConnection connection = new CqlConnection(_cassandraContainer.GetConnectionString()); + + // When + connection.Open(); + + // Then + Assert.Equal(ConnectionState.Open, connection.State); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ExecuteCqlStatementReturnsExpectedResult() + { + // Given + const string selectFromSystemLocalStatement = "SELECT * FROM system.local WHERE key = ?;"; + + using var cluster = Cluster.Builder().WithConnectionString(_cassandraContainer.GetConnectionString()).Build(); + + // When + using var session = cluster.Connect(); + + var preparedStatement = session.Prepare(selectFromSystemLocalStatement); + var boundStatement = preparedStatement.Bind("local"); + using var rowSet = session.Execute(boundStatement); + var rows = rowSet.GetRows().ToImmutableList(); + + // Then + Assert.True(rowSet.IsFullyFetched); + Assert.Single(rows); + Assert.Equal("COMPLETED", rows[0]["bootstrapped"]); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ExecScriptAsyncReturnsSuccess() + { + // Given + const string selectFromSystemLocalStatement = "SELECT * FROM system.local;"; + + // When + var execResult = await _cassandraContainer.ExecScriptAsync(selectFromSystemLocalStatement) + .ConfigureAwait(true); + + // Then + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.Empty(execResult.Stderr); + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj b/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj new file mode 100644 index 000000000..467cd3d93 --- /dev/null +++ b/tests/Testcontainers.Cassandra.Tests/Testcontainers.Cassandra.Tests.csproj @@ -0,0 +1,18 @@ + + + net9.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.Cassandra.Tests/Usings.cs b/tests/Testcontainers.Cassandra.Tests/Usings.cs new file mode 100644 index 000000000..cb1d4b9d6 --- /dev/null +++ b/tests/Testcontainers.Cassandra.Tests/Usings.cs @@ -0,0 +1,8 @@ +global using System.Collections.Immutable; +global using System.Data; +global using System.Data.Common; +global using System.Threading.Tasks; +global using Cassandra; +global using Cassandra.Data; +global using DotNet.Testcontainers.Commons; +global using Xunit; \ No newline at end of file