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 @@ -183,6 +183,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Tests", "tests\Testcontainers.WebDriver.Tests\Testcontainers.WebDriver.Tests.csproj", "{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}"
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.FirebirdSql.Tests", "tests\Testcontainers.FirebirdSql.Tests\Testcontainers.FirebirdSql.Tests.csproj", "{E39095AC-9B34-4178-A486-04C902B6FD33}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -532,6 +536,14 @@ Global
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2}.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
{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
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
Expand Down Expand Up @@ -619,5 +631,7 @@ Global
{1A1983E6-5297-435F-B467-E8E1F11277D6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{27CDB869-A150-4593-958F-6F26E5391E7C} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{31BAF2C4-0608-4C0F-845A-14FE7C0A1670} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{E39095AC-9B34-4178-A486-04C902B6FD33} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
EndGlobalSection
EndGlobal
143 changes: 143 additions & 0 deletions src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
namespace Testcontainers.FirebirdSql;

/// <inheritdoc cref="ContainerBuilder{TBuilderEntity, TContainerEntity, TConfigurationEntity}" />
[PublicAPI]
public 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 FirebirdSysdba = "sysdba";
public const string DefaultSysdbaPassword = "masterkey";

/// <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 dockerResourceConfiguration)
: base(dockerResourceConfiguration)
{
DockerResourceConfiguration = dockerResourceConfiguration;
}

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

/// <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(username: username))
.WithEnvironment("FIREBIRD_USER", FirebirdSysdba.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(password: password))
.WithEnvironment("FIREBIRD_PASSWORD", password)
.WithEnvironment("ISC_PASSWORD", password);
}

/// <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(database: database))
.WithEnvironment("FIREBIRD_DATABASE", database);
}

/// <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()
=> base.Init()
.WithImage(FirebirdSqlImage)
.WithPortBinding(FirebirdSqlPort, true)
.WithDatabase(DefaultDatabase)
.WithUsername(DefaultUsername)
.WithPassword(DefaultPassword)
.WithResourceMapping(Encoding.UTF8.GetBytes(FirebirdSqlContainer.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)
=> Merge(DockerResourceConfiguration, new(resourceConfiguration));

/// <inheritdoc />
protected override FirebirdSqlBuilder Clone(IContainerConfiguration resourceConfiguration)
=> Merge(DockerResourceConfiguration, new(resourceConfiguration));

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

/// <inheritdoc cref="IWaitUntil" />
/// <remarks>
/// Uses the isql Firebird Interactive SQL Utility to detect the readiness of the FirebirdSql container:
/// https://www.firebirdsql.org/file/documentation/html/en/firebirddocs/isql/firebird-isql.html.
/// </remarks>
private sealed class WaitUntil(FirebirdSqlConfiguration configuration) : IWaitUntil
{
private readonly string[] checkDatabaseCommand =
{
"/usr/local/firebird/bin/isql",
"-i",
"/home/firebird_check.sql",
$"localhost:{configuration.Database}",
"-user",
configuration.Username,

Check warning on line 131 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference assignment.

Check warning on line 131 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference assignment.

Check warning on line 131 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference assignment.

Check warning on line 131 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference assignment.
"-pass",
configuration.Password,

Check warning on line 133 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference assignment.

Check warning on line 133 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference assignment.

Check warning on line 133 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference assignment.

Check warning on line 133 in src/Testcontainers.FirebirdSql/FirebirdSqlBuilder.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference assignment.
};

public async Task<bool> UntilAsync(IContainer container)
{
var executionResult = await container.ExecAsync(checkDatabaseCommand)
.ConfigureAwait(false);
return 0L.Equals(executionResult.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 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; }
}
74 changes: 74 additions & 0 deletions src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
namespace Testcontainers.FirebirdSql;

/// <inheritdoc cref="DockerContainer" />
[PublicAPI]
public class FirebirdSqlContainer : DockerContainer, IDatabaseContainer
{
public const string TestQueryString = "select 1 from RDB$DATABASE;";

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;
}

private string GetDatabaseName()
{
if (State == TestcontainersStates.Running && IsFirebird2_5Image)
{
var dbPath = _configuration.Environments.TryGetValue("DBPATH", out var path)
? path
: "/firebird/data";
return $"{dbPath}/{_configuration.Database}";
}
HofmeisterAn marked this conversation as resolved.
Show resolved Hide resolved
else
{
return _configuration.Database;

Check warning on line 32 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference return.

Check warning on line 32 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference return.

Check warning on line 32 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference return.

Check warning on line 32 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference return.
}
}

/// <summary>
/// Indicates whether the used image is an EOL v2.5 version of firebird
/// </summary>
/// <returns>True if the image is a v2.5 version, false otherwise</returns>
public bool IsFirebird2_5Image => Image.Tag.StartsWith("2.5") || Image.Tag.StartsWith("v2.5");

/// <summary>
/// Gets the FirebirdSql connection string.
/// </summary>
/// <returns>The FirebirdSql connection string.</returns>
public string GetConnectionString()
{
var properties = new Dictionary<string, string>();
properties.Add("User", _configuration.Username);

Check warning on line 49 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 49 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 49 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 49 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.
properties.Add("Password", _configuration.Password);

Check warning on line 50 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 50 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-22.04)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 50 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.

Check warning on line 50 in src/Testcontainers.FirebirdSql/FirebirdSqlContainer.cs

View workflow job for this annotation

GitHub Actions / build (windows-2022)

Possible null reference argument for parameter 'value' in 'void Dictionary<string, string>.Add(string key, string value)'.
properties.Add("Database", GetDatabaseName());
properties.Add("DataSource", $"{Hostname}");
properties.Add("Port", $"{GetMappedPublicPort(FirebirdSqlBuilder.FirebirdSqlPort)}");
return string.Join(";", properties.Select(p => $"{p.Key}={p.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", "-user", _configuration.Username, "-pass", _configuration.Password, "-i", scriptFilePath, $"localhost:{_configuration.Database}" }, ct)
.ConfigureAwait(false);
}
}
13 changes: 13 additions & 0 deletions src/Testcontainers.FirebirdSql/Testcontainers.FirebirdSql.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</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;
Loading