From 9a74b7b520e15a4ba300e025d1ca628776a99ec7 Mon Sep 17 00:00:00 2001 From: i-mattanasio Date: Thu, 27 Feb 2025 10:53:53 +0100 Subject: [PATCH] Add Tika service container + basic tests --- Testcontainers.sln | 20 ++- .../Testcontainers.Tika.csproj | 12 ++ src/Testcontainers.Tika/TikaBuilder.cs | 115 ++++++++++++++++++ src/Testcontainers.Tika/TikaConfiguration.cs | 54 ++++++++ src/Testcontainers.Tika/TikaContainer.cs | 28 +++++ src/Testcontainers.Tika/Usings.cs | 8 ++ .../Testcontainers.Tika.Tests.csproj | 19 +++ .../TikaContainerTest.cs | 49 ++++++++ tests/Testcontainers.Tika.Tests/Usings.cs | 5 + 9 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 src/Testcontainers.Tika/Testcontainers.Tika.csproj create mode 100644 src/Testcontainers.Tika/TikaBuilder.cs create mode 100644 src/Testcontainers.Tika/TikaConfiguration.cs create mode 100644 src/Testcontainers.Tika/TikaContainer.cs create mode 100644 src/Testcontainers.Tika/Usings.cs create mode 100644 tests/Testcontainers.Tika.Tests/Testcontainers.Tika.Tests.csproj create mode 100644 tests/Testcontainers.Tika.Tests/TikaContainerTest.cs create mode 100644 tests/Testcontainers.Tika.Tests/Usings.cs diff --git a/Testcontainers.sln b/Testcontainers.sln index 14af32b18..4bcc83ad4 100644 --- a/Testcontainers.sln +++ b/Testcontainers.sln @@ -105,6 +105,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.ServiceBus", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Sftp", "src\Testcontainers.Sftp\Testcontainers.Sftp.csproj", "{7D5C6816-0DD2-4E13-A585-033B5D3C80D5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tika", "src\Testcontainers.Tika\Testcontainers.Tika.csproj", "{AC084DE2-1857-E200-EF47-8C4ADEB2173D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Weaviate", "src\Testcontainers.Weaviate\Testcontainers.Weaviate.csproj", "{68F8600D-24E9-4E03-9E25-5F6EB338EAC1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver", "src\Testcontainers.WebDriver\Testcontainers.WebDriver.csproj", "{64A87DE5-29B0-4A54-9E74-560484D8C7C0}" @@ -233,14 +235,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.WebDriver.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Xunit.Tests", "tests\Testcontainers.Xunit.Tests\Testcontainers.Xunit.Tests.csproj", "{E901DF14-6F05-4FC2-825A-3055FAD33561}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Tika.Tests", "tests\Testcontainers.Tika.Tests\Testcontainers.Tika.Tests.csproj", "{FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5365F780-0E6C-41F0-B1B9-7DC34368F80C}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -682,6 +683,17 @@ Global {E901DF14-6F05-4FC2-825A-3055FAD33561}.Debug|Any CPU.Build.0 = Debug|Any CPU {E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.ActiveCfg = Release|Any CPU {E901DF14-6F05-4FC2-825A-3055FAD33561}.Release|Any CPU.Build.0 = Release|Any CPU + {AC084DE2-1857-E200-EF47-8C4ADEB2173D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AC084DE2-1857-E200-EF47-8C4ADEB2173D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AC084DE2-1857-E200-EF47-8C4ADEB2173D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AC084DE2-1857-E200-EF47-8C4ADEB2173D}.Release|Any CPU.Build.0 = Release|Any CPU + {FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {5365F780-0E6C-41F0-B1B9-7DC34368F80C} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} @@ -794,5 +806,7 @@ Global {DDB41BC8-5826-4D97-9C5F-001151E3FFD6} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {EBA72C3B-57D5-43FF-A5B4-3D55B3B6D4C2} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} {E901DF14-6F05-4FC2-825A-3055FAD33561} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} + {AC084DE2-1857-E200-EF47-8C4ADEB2173D} = {673F23AE-7694-4BB9-ABD4-136D6C13634E} + {FDD2E9F5-DAAC-2F8F-B8DD-6924CD1D9091} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF} EndGlobalSection EndGlobal diff --git a/src/Testcontainers.Tika/Testcontainers.Tika.csproj b/src/Testcontainers.Tika/Testcontainers.Tika.csproj new file mode 100644 index 000000000..b87ee8f03 --- /dev/null +++ b/src/Testcontainers.Tika/Testcontainers.Tika.csproj @@ -0,0 +1,12 @@ + + + net8.0;net9.0;netstandard2.0;netstandard2.1 + latest + + + + + + + + diff --git a/src/Testcontainers.Tika/TikaBuilder.cs b/src/Testcontainers.Tika/TikaBuilder.cs new file mode 100644 index 000000000..1b1044e45 --- /dev/null +++ b/src/Testcontainers.Tika/TikaBuilder.cs @@ -0,0 +1,115 @@ +namespace Testcontainers.Tika; + +/// +[PublicAPI] +public sealed class TikaBuilder : ContainerBuilder +{ + public const string TikaImage = "apache/tika:3.0.0.0-full"; + public const ushort TikaHttpPort = 9998; + + /// + /// Initializes a new instance of the class. + /// + public TikaBuilder() + : this(new TikaConfiguration()) + { + DockerResourceConfiguration = Init().DockerResourceConfiguration; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + private TikaBuilder(TikaConfiguration resourceConfiguration) + : base(resourceConfiguration) + { + DockerResourceConfiguration = resourceConfiguration; + } + /// + + protected override TikaConfiguration DockerResourceConfiguration { get; } + + /// + /// Sets the Tika server timeout. + /// + /// The timeout for the server in milliseconds. + /// A configured instance of . + public TikaBuilder WithTimeout(int timeout) + { + return Merge(DockerResourceConfiguration, new TikaConfiguration(timeout: timeout)) + .WithEnvironment("TIKA_TIMEOUT", timeout.ToString()); + } + + public override TikaContainer Build() + { + Validate(); + return new TikaContainer(DockerResourceConfiguration); + } + + protected override TikaBuilder Init() + { + return base.Init() + .WithImage(TikaImage) + .WithPortBinding(TikaHttpPort, true) + .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new WaitUntil())); + } + + protected override TikaBuilder Clone(IResourceConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new TikaConfiguration(resourceConfiguration)); + } + + protected override TikaBuilder Clone(IContainerConfiguration resourceConfiguration) + { + return Merge(DockerResourceConfiguration, new TikaConfiguration(resourceConfiguration)); + } + + protected override TikaBuilder Merge(TikaConfiguration oldValue, TikaConfiguration newValue) + { + return new TikaBuilder(new TikaConfiguration(oldValue, newValue)); + } + + private sealed class WaitUntil : IWaitUntil + { + private const string HealthCheckPath = "tika"; + private const int MaxRetryAttempts = 10; + private const int DelayInMilliseconds = 1000; + + /// + /// Waits until the Tika server is available by checking the health check endpoint. + /// + /// The container instance to check. + /// + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the Tika server is available. + /// + /// + /// This method sends HTTP GET requests to the Tika server's health check endpoint and retries up to a maximum number of times if the server is not available. + /// + public async Task UntilAsync(DotNet.Testcontainers.Containers.IContainer container) + { + string endpoint = $"http://{container.Hostname}:{container.GetMappedPublicPort(TikaBuilder.TikaHttpPort)}/{HealthCheckPath}"; + + using var client = new HttpClient(); + + for (int i = 0; i < MaxRetryAttempts; i++) + { + try + { + var response = await client.GetAsync(endpoint); + + response.EnsureSuccessStatusCode(); + string responseContent = await response.Content.ReadAsStringAsync(); // This is Tika Server (Apache Tika 3.0.0). Please PUT, volendo si può fare questo check + return true; + } + catch + { + // Ignore exceptions and retry + } + + await Task.Delay(DelayInMilliseconds); + } + + return false; + } + } +} diff --git a/src/Testcontainers.Tika/TikaConfiguration.cs b/src/Testcontainers.Tika/TikaConfiguration.cs new file mode 100644 index 000000000..b7b45bfa6 --- /dev/null +++ b/src/Testcontainers.Tika/TikaConfiguration.cs @@ -0,0 +1,54 @@ +namespace Testcontainers.Tika; + +/// +[PublicAPI] +public sealed class TikaConfiguration : ContainerConfiguration +{ + /// + /// Initializes a new instance of the class. + /// + /// The timeout for the Tika server. + public TikaConfiguration(int timeout = 30000) + { + Timeout = timeout; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Docker resource configuration. + public TikaConfiguration(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 TikaConfiguration(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, + /// combining properties from two existing configurations. + /// + /// The previous configuration values. + /// The new configuration values to merge with the old ones. + public TikaConfiguration(TikaConfiguration oldValue, TikaConfiguration newValue) + : base(oldValue, newValue) + { + // Combine values manually + Timeout = BuildConfiguration.Combine(oldValue.Timeout, newValue.Timeout); + } + + + /// + /// Gets the Tika server timeout. + /// + public int Timeout { get; } +} diff --git a/src/Testcontainers.Tika/TikaContainer.cs b/src/Testcontainers.Tika/TikaContainer.cs new file mode 100644 index 000000000..31552c32a --- /dev/null +++ b/src/Testcontainers.Tika/TikaContainer.cs @@ -0,0 +1,28 @@ +namespace Testcontainers.Tika; + +/// +[PublicAPI] +public sealed class TikaContainer : DockerContainer +{ + private readonly TikaConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The container configuration. + public TikaContainer(TikaConfiguration configuration) + : base(configuration) + { + _configuration = configuration; + } + + /// + /// Gets the Tika connection string. + /// + /// The Tika connection string. + public string GetConnectionString() + { + var endpoint = new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(TikaBuilder.TikaHttpPort)); + return endpoint.ToString(); + } +} diff --git a/src/Testcontainers.Tika/Usings.cs b/src/Testcontainers.Tika/Usings.cs new file mode 100644 index 000000000..ab394a51d --- /dev/null +++ b/src/Testcontainers.Tika/Usings.cs @@ -0,0 +1,8 @@ +global using System; +global using Docker.DotNet.Models; +global using DotNet.Testcontainers.Builders; +global using DotNet.Testcontainers.Configurations; +global using DotNet.Testcontainers.Containers; +global using JetBrains.Annotations; +global using System.Threading.Tasks; +global using System.Net.Http; \ No newline at end of file diff --git a/tests/Testcontainers.Tika.Tests/Testcontainers.Tika.Tests.csproj b/tests/Testcontainers.Tika.Tests/Testcontainers.Tika.Tests.csproj new file mode 100644 index 000000000..e019d24ed --- /dev/null +++ b/tests/Testcontainers.Tika.Tests/Testcontainers.Tika.Tests.csproj @@ -0,0 +1,19 @@ + + + net9.0 + false + false + Debug;Release + + + + + + + + + + + + + diff --git a/tests/Testcontainers.Tika.Tests/TikaContainerTest.cs b/tests/Testcontainers.Tika.Tests/TikaContainerTest.cs new file mode 100644 index 000000000..525edd449 --- /dev/null +++ b/tests/Testcontainers.Tika.Tests/TikaContainerTest.cs @@ -0,0 +1,49 @@ +namespace Testcontainers.Tika.Tests; + +public sealed class TikaContainerTests : IAsyncLifetime +{ + private readonly TikaContainer _tikaContainer = new TikaBuilder().Build(); + + public Task InitializeAsync() + { + return _tikaContainer.StartAsync(); + } + + public Task DisposeAsync() + { + return _tikaContainer.DisposeAsync().AsTask(); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task GetConnectionStringReturnsValidUrl() + { + // When + var connectionString = await Task.Run(() => _tikaContainer.GetConnectionString()); + + // Then + Assert.False(string.IsNullOrEmpty(connectionString)); + Assert.StartsWith("http://", connectionString, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + public async Task TikaHealthCheckShouldBeSuccessful() + { + { + // Given + var httpClient = new HttpClient(); + var connectionString = await Task.Run(() => _tikaContainer.GetConnectionString()); + var requestUrl = $"{connectionString}tika"; + + // When + var response = await httpClient.GetAsync(requestUrl); + + // Then + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(); + Assert.False(string.IsNullOrEmpty(content)); + Assert.StartsWith("This is Tika Server", content); + } + } +} diff --git a/tests/Testcontainers.Tika.Tests/Usings.cs b/tests/Testcontainers.Tika.Tests/Usings.cs new file mode 100644 index 000000000..ed7fe333b --- /dev/null +++ b/tests/Testcontainers.Tika.Tests/Usings.cs @@ -0,0 +1,5 @@ +global using DotNet.Testcontainers.Commons; +global using System; +global using System.Net.Http; +global using System.Threading.Tasks; +global using Xunit;