diff --git a/Testcontainers.sln b/Testcontainers.sln index 7507b0faa..49143c3ac 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer", "src\Testcontainers.FakeGcsServer\Testcontainers.FakeGcsServer.csproj", "{FF86B509-2F9E-4269-ABC2-912B3339DE29}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FirebirdSql", "src\Testcontainers.FirebirdSql\Testcontainers.FirebirdSql.csproj", "{31BAF2C4-0608-4C0F-845A-14FE7C0A1670}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore", "src\Testcontainers.Firestore\Testcontainers.Firestore.csproj", "{B3CC460D-0DFD-48A8-9502-54E9828B7B05}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb", "src\Testcontainers.InfluxDb\Testcontainers.InfluxDb.csproj", "{8F483B83-7BD4-4BD5-9F03-DFC26E1CE678}" @@ -125,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.EventStoreDb EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FakeGcsServer.Tests", "tests\Testcontainers.FakeGcsServer.Tests\Testcontainers.FakeGcsServer.Tests.csproj", "{9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.FirebirdSql.Tests", "tests\Testcontainers.FirebirdSql.Tests\Testcontainers.FirebirdSql.Tests.csproj", "{E39095AC-9B34-4178-A486-04C902B6FD33}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Firestore.Tests", "tests\Testcontainers.Firestore.Tests\Testcontainers.Firestore.Tests.csproj", "{2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.InfluxDb.Tests", "tests\Testcontainers.InfluxDb.Tests\Testcontainers.InfluxDb.Tests.csproj", "{B45B0EF2-5852-4ED3-904A-8FC62A3253D7}" @@ -248,6 +252,10 @@ Global {FF86B509-2F9E-4269-ABC2-912B3339DE29}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.ActiveCfg = Release|Any CPU {FF86B509-2F9E-4269-ABC2-912B3339DE29}.Release|Any CPU.Build.0 = Release|Any CPU + {31BAF2C4-0608-4C0F-845A-14FE7C0A1670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31BAF2C4-0608-4C0F-845A-14FE7C0A1670}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31BAF2C4-0608-4C0F-845A-14FE7C0A1670}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31BAF2C4-0608-4C0F-845A-14FE7C0A1670}.Release|Any CPU.Build.0 = Release|Any CPU {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3CC460D-0DFD-48A8-9502-54E9828B7B05}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -416,6 +424,10 @@ Global {9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA}.Release|Any CPU.Build.0 = Release|Any CPU + {E39095AC-9B34-4178-A486-04C902B6FD33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E39095AC-9B34-4178-A486-04C902B6FD33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E39095AC-9B34-4178-A486-04C902B6FD33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E39095AC-9B34-4178-A486-04C902B6FD33}.Release|Any CPU.Build.0 = Release|Any CPU {2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F0D7CD6-7EA9-46FC-B8F2-25D55699525F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -548,6 +560,7 @@ Global {641DDEA5-B6E0-41E6-BA11-7A28C0913127} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {FF86B509-2F9E-4269-ABC2-912B3339DE29} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {31BAF2C4-0608-4C0F-845A-14FE7C0A1670} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {B3CC460D-0DFD-48A8-9502-54E9828B7B05} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {8F483B83-7BD4-4BD5-9F03-DFC26E1CE678} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} {C5AF86A8-2F11-41B6-BB01-325AD9016B94} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -590,6 +603,7 @@ Global {DD5B3678-468F-4D73-AECE-705E3D66CD43} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {9F27AA1B-C25D-400C-BCB0-6B0BF1A1DCEA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {E39095AC-9B34-4178-A486-04C902B6FD33} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {2F0D7CD6-7EA9-46FC-B8F2-25D55699525F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {B45B0EF2-5852-4ED3-904A-8FC62A3253D7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {BEFC4109-4511-4FBD-AC4F-3D3B388B8CAD} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} diff --git a/src/Testcontainers.FirebirdSql/.editorconfig b/src/Testcontainers.FirebirdSql/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/src/Testcontainers.FirebirdSql/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs b/src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs new file mode 100644 index 000000000..19b957eba --- /dev/null +++ b/src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs @@ -0,0 +1,124 @@ +namespace Testcontainers.FirebirdSql; + +/// +[PublicAPI] +public sealed class FirebirdSqlBuilder : ContainerBuilder +{ + public const string FirebirdSqlImage = "jacobalberty/firebird:v4.0"; + + public const ushort FirebirdSqlPort = 3050; + + public const string DefaultDatabase = "test"; + + public const string DefaultUsername = "test"; + + public const string DefaultPassword = "test"; + + public const string DefaultSysdbaPassword = "masterkey"; + + private const string TestQueryString = "SELECT 1 FROM RDB$DATABASE;"; + + /// + /// Initializes a new instance of the class. + /// + public FirebirdSqlBuilder() + : this(new FirebirdSqlConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public FirebirdSqlBuilder(FirebirdSqlConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + + /// + protected override FirebirdSqlConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the FirebirdSql database. + /// + /// The FirebirdSql database. + /// A configured instance of . + public FirebirdSqlBuilder WithDatabase(string database) + { + return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(database: database)) + .WithEnvironment("FIREBIRD_DATABASE", database); + } + + /// + /// Sets the FirebirdSql username. + /// + /// The FirebirdSql username. + /// A configured instance of . + public FirebirdSqlBuilder WithUsername(string username) + { + return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(username: username)) + .WithEnvironment("FIREBIRD_USER", "sysdba".Equals(username, StringComparison.OrdinalIgnoreCase) ? string.Empty : username); + } + + /// + /// Sets the FirebirdSql password. + /// + /// The FirebirdSql password. + /// A configured instance of . + public FirebirdSqlBuilder WithPassword(string password) + { + return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(password: password)) + .WithEnvironment("FIREBIRD_PASSWORD", password) + .WithEnvironment("ISC_PASSWORD", password); + } + + /// + public override FirebirdSqlContainer Build() + { + Validate(); + return new FirebirdSqlContainer(DockerResourceConfiguration, TestcontainersSettings.Logger); + } + + /// + protected override FirebirdSqlBuilder Init() + { + return base.Init() + .WithImage(FirebirdSqlImage) + .WithPortBinding(FirebirdSqlPort, true) + .WithDatabase(DefaultDatabase) + .WithUsername(DefaultUsername) + .WithPassword(DefaultPassword) + .WithResourceMapping(Encoding.Default.GetBytes(TestQueryString), "/home/firebird_check.sql") + .WithWaitStrategy(Wait.ForUnixContainer().UntilContainerIsHealthy()); + } + + /// + protected override void Validate() + { + base.Validate(); + + _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password)) + .NotNull() + .NotEmpty(); + } + + /// + protected override FirebirdSqlBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(resourceConfiguration)); + } + + /// + protected override FirebirdSqlBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(resourceConfiguration)); + } + + /// + protected override FirebirdSqlBuilder Merge(FirebirdSqlConfiguration oldValue, FirebirdSqlConfiguration newValue) + { + return new FirebirdSqlBuilder(new FirebirdSqlConfiguration(oldValue, newValue)); + } +} \ No newline at end of file diff --git a/src/Testcontainers.FirebirdSql/FirebirdSqlConfiguration.cs b/src/Testcontainers.FirebirdSql/FirebirdSqlConfiguration.cs new file mode 100644 index 000000000..26b799d3f --- /dev/null +++ b/src/Testcontainers.FirebirdSql/FirebirdSqlConfiguration.cs @@ -0,0 +1,82 @@ +namespace Testcontainers.FirebirdSql; + +/// +[PublicAPI] +public sealed class FirebirdSqlConfiguration : ContainerConfiguration +{ + private readonly string _database; + + /// + /// Initializes a new instance of the class. + /// + /// The FirebirdSql database. + /// The FirebirdSql username. + /// The FirebirdSql password. + public FirebirdSqlConfiguration( + string database = null, + string username = null, + string password = null) + { + _database = database; + Username = username; + Password = password; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public FirebirdSqlConfiguration(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 FirebirdSqlConfiguration(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 FirebirdSqlConfiguration(FirebirdSqlConfiguration resourceConfiguration) + : this(new FirebirdSqlConfiguration(), 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 FirebirdSqlConfiguration(FirebirdSqlConfiguration oldValue, FirebirdSqlConfiguration newValue) + : base(oldValue, newValue) + { + _database = BuildConfiguration.Combine(oldValue._database, newValue._database); + Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username); + Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password); + } + + /// + /// Gets the FirebirdSql database. + /// + public string Database => Image.Tag.StartsWith("2.5") || Image.Tag.StartsWith("v2.5") ? string.Join("/", "/firebird/data", _database) : _database; + + /// + /// Gets the FirebirdSql username. + /// + public string Username { get; } + + /// + /// Gets the FirebirdSql password. + /// + public string Password { get; } +} \ No newline at end of file diff --git a/src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs b/src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs new file mode 100644 index 000000000..f12568949 --- /dev/null +++ b/src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs @@ -0,0 +1,51 @@ +namespace Testcontainers.FirebirdSql; + +/// +[PublicAPI] +public sealed class FirebirdSqlContainer : DockerContainer, IDatabaseContainer +{ + private readonly FirebirdSqlConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + /// The logger. + public FirebirdSqlContainer(FirebirdSqlConfiguration configuration, ILogger logger) + : base(configuration, logger) + { + _configuration = configuration; + } + + /// + /// Gets the FirebirdSql connection string. + /// + /// The FirebirdSql connection string. + public string GetConnectionString() + { + var properties = new Dictionary(); + properties.Add("DataSource", Hostname); + properties.Add("Port", GetMappedPublicPort(FirebirdSqlBuilder.FirebirdSqlPort).ToString()); + properties.Add("Database", _configuration.Database); + properties.Add("User", _configuration.Username); + properties.Add("Password", _configuration.Password); + return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value))); + } + + /// + /// Executes the SQL script in the FirebirdSql container. + /// + /// The content of the SQL script to execute. + /// Cancellation token. + /// Task that completes when the SQL 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[] { "/usr/local/firebird/bin/isql", "-i", scriptFilePath, "-user", _configuration.Username, "-pass", _configuration.Password, _configuration.Database }, ct) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Testcontainers.FirebirdSql/Testcontainers.FirebirdSql.csproj b/src/Testcontainers.FirebirdSql/Testcontainers.FirebirdSql.csproj new file mode 100644 index 000000000..7048179d1 --- /dev/null +++ b/src/Testcontainers.FirebirdSql/Testcontainers.FirebirdSql.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0;netstandard2.1 + latest + + + + + + + + \ No newline at end of file diff --git a/src/Testcontainers.FirebirdSql/Usings.cs b/src/Testcontainers.FirebirdSql/Usings.cs new file mode 100644 index 000000000..a01abde62 --- /dev/null +++ b/src/Testcontainers.FirebirdSql/Usings.cs @@ -0,0 +1,14 @@ +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/tests/Testcontainers.FirebirdSql.Tests/.editorconfig b/tests/Testcontainers.FirebirdSql.Tests/.editorconfig new file mode 100644 index 000000000..6f066619d --- /dev/null +++ b/tests/Testcontainers.FirebirdSql.Tests/.editorconfig @@ -0,0 +1 @@ +root = true \ No newline at end of file diff --git a/tests/Testcontainers.FirebirdSql.Tests/FirebirdSqlContainerTest.cs b/tests/Testcontainers.FirebirdSql.Tests/FirebirdSqlContainerTest.cs new file mode 100644 index 000000000..1537a6d8f --- /dev/null +++ b/tests/Testcontainers.FirebirdSql.Tests/FirebirdSqlContainerTest.cs @@ -0,0 +1,96 @@ +namespace Testcontainers.FirebirdSql; + +public abstract class FirebirdSqlContainerTest : IAsyncLifetime +{ + private readonly FirebirdSqlContainer _firebirdSqlContainer; + + private FirebirdSqlContainerTest(FirebirdSqlContainer firebirdSqlContainer) + { + _firebirdSqlContainer = firebirdSqlContainer; + } + + public Task InitializeAsync() + { + return _firebirdSqlContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _firebirdSqlContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public void ConnectionStateReturnsOpen() + { + // Given + using DbConnection connection = new FbConnection(_firebirdSqlContainer.GetConnectionString()); + + // When + connection.Open(); + + // Then + Assert.Equal(ConnectionState.Open, connection.State); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task ExecScriptReturnsSuccessful() + { + // Given + const string scriptContent = "SELECT 1 FROM RDB$DATABASE;"; + + // When + var execResult = await _firebirdSqlContainer.ExecScriptAsync(scriptContent) + .ConfigureAwait(false); + + // When + Assert.True(0L.Equals(execResult.ExitCode), execResult.Stderr); + Assert.Empty(execResult.Stderr); + } + + [UsedImplicitly] + public sealed class FirebirdSql25Sc : FirebirdSqlContainerTest + { + public FirebirdSql25Sc() + : base(new FirebirdSqlBuilder().WithImage("jacobalberty/firebird:2.5-sc").Build()) + { + } + } + + [UsedImplicitly] + public sealed class FirebirdSql25Ss : FirebirdSqlContainerTest + { + public FirebirdSql25Ss() + : base(new FirebirdSqlBuilder().WithImage("jacobalberty/firebird:2.5-ss").Build()) + { + } + } + + [UsedImplicitly] + public sealed class FirebirdSql30 : FirebirdSqlContainerTest + { + public FirebirdSql30() + : base(new FirebirdSqlBuilder().WithImage("jacobalberty/firebird:v3.0").Build()) + { + } + } + + [UsedImplicitly] + public sealed class FirebirdSql40 : FirebirdSqlContainerTest + { + public FirebirdSql40() + : base(new FirebirdSqlBuilder().WithImage("jacobalberty/firebird:v4.0").Build()) + { + } + } + + [UsedImplicitly] + public sealed class FirebirdSqlSysdba : FirebirdSqlContainerTest + { + public FirebirdSqlSysdba() + : base(new FirebirdSqlBuilder().WithUsername("sysdba").WithPassword("some-password").Build()) + { + } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.FirebirdSql.Tests/Testcontainers.FirebirdSql.Tests.csproj b/tests/Testcontainers.FirebirdSql.Tests/Testcontainers.FirebirdSql.Tests.csproj new file mode 100644 index 000000000..39556943d --- /dev/null +++ b/tests/Testcontainers.FirebirdSql.Tests/Testcontainers.FirebirdSql.Tests.csproj @@ -0,0 +1,18 @@ + + + net8.0 + false + false + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Testcontainers.FirebirdSql.Tests/Usings.cs b/tests/Testcontainers.FirebirdSql.Tests/Usings.cs new file mode 100644 index 000000000..28861332f --- /dev/null +++ b/tests/Testcontainers.FirebirdSql.Tests/Usings.cs @@ -0,0 +1,7 @@ +global using System.Data; +global using System.Data.Common; +global using System.Threading.Tasks; +global using DotNet.Testcontainers.Commons; +global using FirebirdSql.Data.FirebirdClient; +global using JetBrains.Annotations; +global using Xunit; \ No newline at end of file