Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Firebird SQL module #1073

Merged
merged 10 commits into from
Jan 2, 2024
14 changes: 14 additions & 0 deletions Testcontainers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions src/Testcontainers.FirebirdSql/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
153 changes: 153 additions & 0 deletions src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
namespace Testcontainers.FirebirdSql;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public sealed class FirebirdSqlBuilder : ContainerBuilder<FirebirdSqlBuilder, FirebirdSqlContainer, FirebirdSqlConfiguration>
{
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;";
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlBuilder" /> class.
/// </summary>
public FirebirdSqlBuilder()
: this(new FirebirdSqlConfiguration())
{
DockerResourceConfiguration = Init().DockerResourceConfiguration;
}

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlBuilder" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FirebirdSqlBuilder(FirebirdSqlConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
DockerResourceConfiguration = resourceConfiguration;
}

/// <inheritdoc />
protected override FirebirdSqlConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the FirebirdSql database.
/// </summary>
/// <param name="database">The FirebirdSql database.</param>
/// <returns>A configured instance of <see cref="FirebirdSqlBuilder" />.</returns>
public FirebirdSqlBuilder WithDatabase(string database)
{
return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(database: database))
.WithEnvironment("FIREBIRD_DATABASE", database);
}

/// <summary>
/// Sets the FirebirdSql username.
/// </summary>
/// <param name="username">The FirebirdSql username.</param>
/// <returns>A configured instance of <see cref="FirebirdSqlBuilder" />.</returns>
public FirebirdSqlBuilder WithUsername(string username)
{
return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(username: username))
.WithEnvironment("FIREBIRD_USER", "sysdba".Equals(username, StringComparison.OrdinalIgnoreCase) ? string.Empty : username);
}

/// <summary>
/// Sets the FirebirdSql password.
/// </summary>
/// <param name="password">The FirebirdSql password.</param>
/// <returns>A configured instance of <see cref="FirebirdSqlBuilder" />.</returns>
public FirebirdSqlBuilder WithPassword(string password)
{
return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(password: password))
.WithEnvironment("FIREBIRD_PASSWORD", password)
.WithEnvironment("ISC_PASSWORD", password);
}

/// <inheritdoc />
public override FirebirdSqlContainer Build()
{
Validate();

var compoundWaitStrategy = Wait.ForUnixContainer()
.UntilContainerIsHealthy()
.AddCustomWaitStrategy(new WaitUntil(DockerResourceConfiguration));
return new FirebirdSqlContainer(
WithWaitStrategy(compoundWaitStrategy).DockerResourceConfiguration,
TestcontainersSettings.Logger);
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc />
protected override FirebirdSqlBuilder Init()
{
return base.Init()
.WithImage(FirebirdSqlImage)
.WithPortBinding(FirebirdSqlPort, true)
.WithDatabase(DefaultDatabase)
.WithUsername(DefaultUsername)
.WithPassword(DefaultPassword)
.WithResourceMapping(Encoding.UTF8.GetBytes(TestQueryString), "/home/firebird_check.sql");
}

/// <inheritdoc />
protected override void Validate()
{
base.Validate();

_ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
.NotNull()
.NotEmpty();
}

/// <inheritdoc />
protected override FirebirdSqlBuilder Clone(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override FirebirdSqlBuilder Clone(IContainerConfiguration resourceConfiguration)
{
return Merge(DockerResourceConfiguration, new FirebirdSqlConfiguration(resourceConfiguration));
}

/// <inheritdoc />
protected override FirebirdSqlBuilder Merge(FirebirdSqlConfiguration oldValue, FirebirdSqlConfiguration newValue)
{
return new FirebirdSqlBuilder(new FirebirdSqlConfiguration(oldValue, newValue));
}

/// <inheritdoc cref="IWaitUntil" />
private sealed class WaitUntil : IWaitUntil
{
private readonly IList<string> _command;

/// <summary>
/// Initializes a new instance of the <see cref="WaitUntil" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
public WaitUntil(FirebirdSqlConfiguration configuration)
{
_command = new List<string> { "/usr/local/firebird/bin/isql", "-i", "/home/firebird_check.sql", $"localhost:{configuration.Database}", "-user", configuration.Username, "-pass", configuration.Password };
}

/// <inheritdoc />
public async Task<bool> UntilAsync(IContainer container)
{
var execResult = await container.ExecAsync(_command)
.ConfigureAwait(false);

return 0L.Equals(execResult.ExitCode);
}
}
}
80 changes: 80 additions & 0 deletions src/Testcontainers.FirebirdSql/FirebirdSqlConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Testcontainers.FirebirdSql;

/// <inheritdoc cref="ContainerConfiguration" />
[PublicAPI]
public sealed class FirebirdSqlConfiguration : ContainerConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlConfiguration" /> class.
/// </summary>
/// <param name="database">The FirebirdSql database.</param>
/// <param name="username">The FirebirdSql username.</param>
/// <param name="password">The FirebirdSql password.</param>
public FirebirdSqlConfiguration(
string database = null,
string username = null,
string password = null)
{
Database = database;
Username = username;
Password = password;
}

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FirebirdSqlConfiguration(IResourceConfiguration<CreateContainerParameters> resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FirebirdSqlConfiguration(IContainerConfiguration resourceConfiguration)
: base(resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlConfiguration" /> class.
/// </summary>
/// <param name="resourceConfiguration">The Docker resource configuration.</param>
public FirebirdSqlConfiguration(FirebirdSqlConfiguration resourceConfiguration)
: this(new FirebirdSqlConfiguration(), resourceConfiguration)
{
// Passes the configuration upwards to the base implementations to create an updated immutable copy.
}

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlConfiguration" /> class.
/// </summary>
/// <param name="oldValue">The old Docker resource configuration.</param>
/// <param name="newValue">The new Docker resource configuration.</param>
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);
}

/// <summary>
/// Gets the FirebirdSql database.
/// </summary>
public string Database { get; }

/// <summary>
/// Gets the FirebirdSql username.
/// </summary>
public string Username { get; }

/// <summary>
/// Gets the FirebirdSql password.
/// </summary>
public string Password { get; }
}
62 changes: 62 additions & 0 deletions src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Testcontainers.FirebirdSql;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public sealed class FirebirdSqlContainer : DockerContainer, IDatabaseContainer
{
private readonly FirebirdSqlConfiguration _configuration;

/// <summary>
/// Initializes a new instance of the <see cref="FirebirdSqlContainer" /> class.
/// </summary>
/// <param name="configuration">The container configuration.</param>
/// <param name="logger">The logger.</param>
public FirebirdSqlContainer(FirebirdSqlConfiguration configuration, ILogger logger)
: base(configuration, logger)
{
_configuration = configuration;
}

/// <summary>
/// Gets the FirebirdSql connection string.
/// </summary>
/// <returns>The FirebirdSql connection string.</returns>
public string GetConnectionString()
{
string database;

if (_configuration.Image.Tag.StartsWith("2.5") || _configuration.Image.Tag.StartsWith("v2.5"))
{
database = string.Join("/", [_configuration.Environments.TryGetValue("DBPATH", out var dbPath) && !string.IsNullOrEmpty(dbPath) ? dbPath : "/firebird/data", _configuration.Database]);
}
else
{
database = _configuration.Database;
}
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved

var properties = new Dictionary<string, string>();
properties.Add("DataSource", Hostname);
properties.Add("Port", GetMappedPublicPort(FirebirdSqlBuilder.FirebirdSqlPort).ToString());
properties.Add("Database", database);
properties.Add("User", _configuration.Username);
properties.Add("Password", _configuration.Password);
return string.Join(";", properties.Select(property => string.Join("=", property.Key, property.Value)));
}

/// <summary>
/// Executes the SQL script in the FirebirdSql container.
/// </summary>
/// <param name="scriptContent">The content of the SQL script to execute.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Task that completes when the SQL script has been executed.</returns>
public async Task<ExecResult> 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, $"localhost:{_configuration.Database}", "-user", _configuration.Username, "-pass", _configuration.Password }, ct)
.ConfigureAwait(false);
}
}
12 changes: 12 additions & 0 deletions src/Testcontainers.FirebirdSql/Testcontainers.FirebirdSql.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)src/Testcontainers/Testcontainers.csproj"/>
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions src/Testcontainers.FirebirdSql/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions tests/Testcontainers.FirebirdSql.Tests/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
root = true
Loading