From e96d4e6cb68c999847cdd94b1c826297034af2f2 Mon Sep 17 00:00:00 2001 From: Stefan Schranz Date: Thu, 3 Oct 2024 19:28:28 +0200 Subject: [PATCH] feat: Add Neo4j Enterprise Edition support (`WithEnterpriseEdition(bool)`) (#1269) Co-authored-by: Andre Hofmeister <9199345+HofmeisterAn@users.noreply.github.com> --- docs/modules/neo4j.md | 9 ++- src/Testcontainers.Neo4j/Neo4jBuilder.cs | 77 +++++++++++++++++++ src/Testcontainers.Neo4j/Usings.cs | 4 + .../Neo4jBuilderTest.cs | 50 ++++++++++++ .../Neo4jContainerTest.cs | 54 +++++++++++-- tests/Testcontainers.Neo4j.Tests/Usings.cs | 2 + 6 files changed, 188 insertions(+), 8 deletions(-) create mode 100644 tests/Testcontainers.Neo4j.Tests/Neo4jBuilderTest.cs diff --git a/docs/modules/neo4j.md b/docs/modules/neo4j.md index 6f6f0d12b..abcb9b6a1 100644 --- a/docs/modules/neo4j.md +++ b/docs/modules/neo4j.md @@ -8,7 +8,14 @@ Add the following dependency to your project file: dotnet add package Testcontainers.Neo4j ``` -You can start an Neo4j container instance from any .NET application. This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method. +You can start an Neo4j container instance from any .NET application. Here, we create different container instances and pass them to the base test class. This allows us to test different configurations. + +=== "Create Container Instance" + ```csharp + --8<-- "tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs:CreateNeo4jContainer" + ``` + +This example uses xUnit.net's `IAsyncLifetime` interface to manage the lifecycle of the container. The container is started in the `InitializeAsync` method before the test method runs, ensuring that the environment is ready for testing. After the test completes, the container is removed in the `DisposeAsync` method. === "Usage Example" ```csharp diff --git a/src/Testcontainers.Neo4j/Neo4jBuilder.cs b/src/Testcontainers.Neo4j/Neo4jBuilder.cs index 54770e961..14013d278 100644 --- a/src/Testcontainers.Neo4j/Neo4jBuilder.cs +++ b/src/Testcontainers.Neo4j/Neo4jBuilder.cs @@ -10,6 +10,12 @@ public sealed class Neo4jBuilder : ContainerBuilder /// Initializes a new instance of the class. /// @@ -32,6 +38,63 @@ private Neo4jBuilder(Neo4jConfiguration resourceConfiguration) /// protected override Neo4jConfiguration DockerResourceConfiguration { get; } + /// + /// Sets the image to the Neo4j Enterprise Edition. + /// + /// + /// When is set to true, the Neo4j Enterprise Edition license is accepted. + /// If the Community Edition is explicitly used, we do not update the image. + /// + /// A boolean value indicating whether the Neo4j Enterprise Edition license agreement is accepted. + /// A configured instance of . + public Neo4jBuilder WithEnterpriseEdition(bool acceptLicenseAgreement) + { + const string communitySuffix = "community"; + + const string enterpriseSuffix = "enterprise"; + + var operatingSystems = new[] { "bullseye", "ubi9" }; + + var image = DockerResourceConfiguration.Image; + + string tag; + + // If the specified image does not contain a tag (but a digest), we cannot determine the + // actual version and append the enterprise suffix. We expect the developer to set the + // Enterprise Edition. + if (image.Tag == null) + { + tag = null; + } + else if (image.MatchLatestOrNightly()) + { + tag = enterpriseSuffix; + } + else if (image.MatchVersion(v => v.Contains(communitySuffix))) + { + tag = image.Tag; + } + else if (image.MatchVersion(v => v.Contains(enterpriseSuffix))) + { + tag = image.Tag; + } + else if (image.MatchVersion(v => operatingSystems.Any(v.Contains))) + { + MatchEvaluator evaluator = match => $"{enterpriseSuffix}-{match.Value}"; + tag = Regex.Replace(image.Tag, string.Join("|", operatingSystems), evaluator); + } + else + { + tag = $"{image.Tag}-{enterpriseSuffix}"; + } + + var enterpriseImage = new DockerImage(image.Repository, image.Registry, tag, tag == null ? image.Digest : null); + + var licenseAgreement = acceptLicenseAgreement ? AcceptLicenseAgreement : DeclineLicenseAgreement; + + return WithImage(enterpriseImage).WithEnvironment(AcceptLicenseAgreementEnvVar, licenseAgreement); + } + /// public override Neo4jContainer Build() { @@ -39,6 +102,20 @@ public override Neo4jContainer Build() return new Neo4jContainer(DockerResourceConfiguration); } + /// + protected override void Validate() + { + const string message = "The image '{0}' requires you to accept a license agreement."; + + base.Validate(); + + Predicate licenseAgreementNotAccepted = value => value.Image.Tag != null && value.Image.Tag.Contains("enterprise") + && (!value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal)); + + _ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image)) + .ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => throw new ArgumentException(string.Format(message, DockerResourceConfiguration.Image.FullName), argument.Name)); + } + /// protected override Neo4jBuilder Init() { diff --git a/src/Testcontainers.Neo4j/Usings.cs b/src/Testcontainers.Neo4j/Usings.cs index 79fd3af9b..adef2193a 100644 --- a/src/Testcontainers.Neo4j/Usings.cs +++ b/src/Testcontainers.Neo4j/Usings.cs @@ -1,6 +1,10 @@ global using System; +global using System.Linq; +global using System.Text.RegularExpressions; 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 DotNet.Testcontainers.Images; global using JetBrains.Annotations; \ No newline at end of file diff --git a/tests/Testcontainers.Neo4j.Tests/Neo4jBuilderTest.cs b/tests/Testcontainers.Neo4j.Tests/Neo4jBuilderTest.cs new file mode 100644 index 000000000..fc8b15b1e --- /dev/null +++ b/tests/Testcontainers.Neo4j.Tests/Neo4jBuilderTest.cs @@ -0,0 +1,50 @@ +namespace Testcontainers.Neo4j; + +public sealed class Neo4jBuilderTest +{ + [Theory] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + [InlineData("neo4j:5.23.0", "5.23.0-enterprise")] + [InlineData("neo4j:5.23", "5.23-enterprise")] + [InlineData("neo4j:5", "5-enterprise")] + [InlineData("neo4j:5.23.0-community", "5.23.0-community")] + [InlineData("neo4j:5.23-community", "5.23-community")] + [InlineData("neo4j:5-community", "5-community")] + [InlineData("neo4j:community", "community")] + [InlineData("neo4j:5.23.0-bullseye", "5.23.0-enterprise-bullseye")] + [InlineData("neo4j:5.23-bullseye", "5.23-enterprise-bullseye")] + [InlineData("neo4j:5-bullseye", "5-enterprise-bullseye")] + [InlineData("neo4j:bullseye", "enterprise-bullseye")] + [InlineData("neo4j:5.23.0-enterprise-bullseye", "5.23.0-enterprise-bullseye")] + [InlineData("neo4j:5.23-enterprise-bullseye", "5.23-enterprise-bullseye")] + [InlineData("neo4j:5-enterprise-bullseye", "5-enterprise-bullseye")] + [InlineData("neo4j:enterprise-bullseye", "enterprise-bullseye")] + [InlineData("neo4j:5.23.0-enterprise", "5.23.0-enterprise")] + [InlineData("neo4j:5.23-enterprise", "5.23-enterprise")] + [InlineData("neo4j:5-enterprise", "5-enterprise")] + [InlineData("neo4j:enterprise", "enterprise")] + [InlineData("neo4j", "enterprise")] + [InlineData("neo4j@sha256:20eb19e3d60f9f07c12c89eac8d8722e393be7e45c6d7e56004a2c493b8e2032", null)] + public void AppendsEnterpriseSuffixWhenEnterpriseEditionLicenseAgreementIsAccepted(string image, string expected) + { + var neo4jContainer = new Neo4jBuilder().WithImage(image).WithEnterpriseEdition(true).Build(); + Assert.Equal(expected, neo4jContainer.Image.Tag); + } + + [Theory] + [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] + [ClassData(typeof(Neo4jBuilderConfigurations))] + public void ThrowsArgumentExceptionWhenEnterpriseEditionLicenseAgreementIsNotAccepted(Neo4jBuilder neo4jBuilder) + { + Assert.Throws(neo4jBuilder.Build); + } + + private sealed class Neo4jBuilderConfigurations : TheoryData + { + public Neo4jBuilderConfigurations() + { + Add(new Neo4jBuilder().WithImage(Neo4jBuilder.Neo4jImage + "-enterprise")); + Add(new Neo4jBuilder().WithEnterpriseEdition(false)); + } + } +} \ No newline at end of file diff --git a/tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs b/tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs index 460b38934..b81a48eac 100644 --- a/tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs +++ b/tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs @@ -1,10 +1,17 @@ namespace Testcontainers.Neo4j; -public sealed class Neo4jContainerTest : IAsyncLifetime +public abstract class Neo4jContainerTest : IAsyncLifetime { - // # --8<-- [start:UseNeo4jContainer] - private readonly Neo4jContainer _neo4jContainer = new Neo4jBuilder().Build(); + private readonly Neo4jContainer _neo4jContainer; + + private Neo4jContainerTest(Neo4jContainer neo4jContainer) + { + _neo4jContainer = neo4jContainer; + } + + public abstract string Edition { get; } + // # --8<-- [start:UseNeo4jContainer] public Task InitializeAsync() { return _neo4jContainer.StartAsync(); @@ -17,18 +24,51 @@ public Task DisposeAsync() [Fact] [Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))] - public void SessionReturnsDatabase() + public async Task SessionReturnsDatabase() { // Given - const string database = "neo4j"; + const string neo4jDatabase = "neo4j"; using var driver = GraphDatabase.Driver(_neo4jContainer.GetConnectionString()); // When - using var session = driver.AsyncSession(sessionConfigBuilder => sessionConfigBuilder.WithDatabase(database)); + using var session = driver.AsyncSession(sessionConfigBuilder => sessionConfigBuilder.WithDatabase(neo4jDatabase)); + + var result = await session.RunAsync("CALL dbms.components() YIELD edition RETURN edition") + .ConfigureAwait(true); + + var record = await result.SingleAsync() + .ConfigureAwait(true); + + var edition = record["edition"].As(); // Then - Assert.Equal(database, session.SessionConfig.Database); + Assert.Equal(neo4jDatabase, session.SessionConfig.Database); + Assert.Equal(Edition, edition); } // # --8<-- [end:UseNeo4jContainer] + + // # --8<-- [start:CreateNeo4jContainer] + [UsedImplicitly] + public sealed class Neo4jDefaultConfiguration : Neo4jContainerTest + { + public Neo4jDefaultConfiguration() + : base(new Neo4jBuilder().Build()) + { + } + + public override string Edition => "community"; + } + + [UsedImplicitly] + public sealed class Neo4jEnterpriseEditionConfiguration : Neo4jContainerTest + { + public Neo4jEnterpriseEditionConfiguration() + : base(new Neo4jBuilder().WithEnterpriseEdition(true).Build()) + { + } + + public override string Edition => "enterprise"; + } + // # --8<-- [end:CreateNeo4jContainer] } \ No newline at end of file diff --git a/tests/Testcontainers.Neo4j.Tests/Usings.cs b/tests/Testcontainers.Neo4j.Tests/Usings.cs index a6d11cfdb..b7288ff63 100644 --- a/tests/Testcontainers.Neo4j.Tests/Usings.cs +++ b/tests/Testcontainers.Neo4j.Tests/Usings.cs @@ -1,4 +1,6 @@ +global using System; global using System.Threading.Tasks; global using DotNet.Testcontainers.Commons; +global using JetBrains.Annotations; global using Neo4j.Driver; global using Xunit; \ No newline at end of file