Skip to content

Commit

Permalink
feat: Add Neo4j Enterprise Edition support (`WithEnterpriseEdition(bo…
Browse files Browse the repository at this point in the history
…ol)`) (#1269)

Co-authored-by: Andre Hofmeister <[email protected]>
  • Loading branch information
Sossenbinder and HofmeisterAn authored Oct 3, 2024
1 parent 0b8b34b commit e96d4e6
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 8 deletions.
9 changes: 8 additions & 1 deletion docs/modules/neo4j.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 77 additions & 0 deletions src/Testcontainers.Neo4j/Neo4jBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public sealed class Neo4jBuilder : ContainerBuilder<Neo4jBuilder, Neo4jContainer

public const ushort Neo4jBoltPort = 7687;

private const string AcceptLicenseAgreementEnvVar = "NEO4J_ACCEPT_LICENSE_AGREEMENT";

private const string AcceptLicenseAgreement = "yes";

private const string DeclineLicenseAgreement = "no";

/// <summary>
/// Initializes a new instance of the <see cref="Neo4jBuilder" /> class.
/// </summary>
Expand All @@ -32,13 +38,84 @@ private Neo4jBuilder(Neo4jConfiguration resourceConfiguration)
/// <inheritdoc />
protected override Neo4jConfiguration DockerResourceConfiguration { get; }

/// <summary>
/// Sets the image to the Neo4j Enterprise Edition.
/// </summary>
/// <remarks>
/// When <paramref name="acceptLicenseAgreement" /> is set to <c>true</c>, the Neo4j Enterprise Edition <see href="https://neo4j.com/docs/operations-manual/current/docker/introduction/#_neo4j_editions">license</see> is accepted.
/// If the Community Edition is explicitly used, we do not update the image.
/// </remarks>
/// <param name="acceptLicenseAgreement">A boolean value indicating whether the Neo4j Enterprise Edition license agreement is accepted.</param>
/// <returns>A configured instance of <see cref="Neo4jBuilder" />.</returns>
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);
}

/// <inheritdoc />
public override Neo4jContainer Build()
{
Validate();
return new Neo4jContainer(DockerResourceConfiguration);
}

/// <inheritdoc />
protected override void Validate()
{
const string message = "The image '{0}' requires you to accept a license agreement.";

base.Validate();

Predicate<Neo4jConfiguration> 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));
}

/// <inheritdoc />
protected override Neo4jBuilder Init()
{
Expand Down
4 changes: 4 additions & 0 deletions src/Testcontainers.Neo4j/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions tests/Testcontainers.Neo4j.Tests/Neo4jBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentException>(neo4jBuilder.Build);
}

private sealed class Neo4jBuilderConfigurations : TheoryData<Neo4jBuilder>
{
public Neo4jBuilderConfigurations()
{
Add(new Neo4jBuilder().WithImage(Neo4jBuilder.Neo4jImage + "-enterprise"));
Add(new Neo4jBuilder().WithEnterpriseEdition(false));
}
}
}
54 changes: 47 additions & 7 deletions tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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<string>();

// 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]
}
2 changes: 2 additions & 0 deletions tests/Testcontainers.Neo4j.Tests/Usings.cs
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit e96d4e6

Please sign in to comment.