Skip to content

Commit e96d4e6

Browse files
feat: Add Neo4j Enterprise Edition support (WithEnterpriseEdition(bool)) (#1269)
Co-authored-by: Andre Hofmeister <[email protected]>
1 parent 0b8b34b commit e96d4e6

File tree

6 files changed

+188
-8
lines changed

6 files changed

+188
-8
lines changed

docs/modules/neo4j.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ Add the following dependency to your project file:
88
dotnet add package Testcontainers.Neo4j
99
```
1010

11-
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.
11+
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.
12+
13+
=== "Create Container Instance"
14+
```csharp
15+
--8<-- "tests/Testcontainers.Neo4j.Tests/Neo4jContainerTest.cs:CreateNeo4jContainer"
16+
```
17+
18+
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.
1219

1320
=== "Usage Example"
1421
```csharp

src/Testcontainers.Neo4j/Neo4jBuilder.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ public sealed class Neo4jBuilder : ContainerBuilder<Neo4jBuilder, Neo4jContainer
1010

1111
public const ushort Neo4jBoltPort = 7687;
1212

13+
private const string AcceptLicenseAgreementEnvVar = "NEO4J_ACCEPT_LICENSE_AGREEMENT";
14+
15+
private const string AcceptLicenseAgreement = "yes";
16+
17+
private const string DeclineLicenseAgreement = "no";
18+
1319
/// <summary>
1420
/// Initializes a new instance of the <see cref="Neo4jBuilder" /> class.
1521
/// </summary>
@@ -32,13 +38,84 @@ private Neo4jBuilder(Neo4jConfiguration resourceConfiguration)
3238
/// <inheritdoc />
3339
protected override Neo4jConfiguration DockerResourceConfiguration { get; }
3440

41+
/// <summary>
42+
/// Sets the image to the Neo4j Enterprise Edition.
43+
/// </summary>
44+
/// <remarks>
45+
/// 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.
46+
/// If the Community Edition is explicitly used, we do not update the image.
47+
/// </remarks>
48+
/// <param name="acceptLicenseAgreement">A boolean value indicating whether the Neo4j Enterprise Edition license agreement is accepted.</param>
49+
/// <returns>A configured instance of <see cref="Neo4jBuilder" />.</returns>
50+
public Neo4jBuilder WithEnterpriseEdition(bool acceptLicenseAgreement)
51+
{
52+
const string communitySuffix = "community";
53+
54+
const string enterpriseSuffix = "enterprise";
55+
56+
var operatingSystems = new[] { "bullseye", "ubi9" };
57+
58+
var image = DockerResourceConfiguration.Image;
59+
60+
string tag;
61+
62+
// If the specified image does not contain a tag (but a digest), we cannot determine the
63+
// actual version and append the enterprise suffix. We expect the developer to set the
64+
// Enterprise Edition.
65+
if (image.Tag == null)
66+
{
67+
tag = null;
68+
}
69+
else if (image.MatchLatestOrNightly())
70+
{
71+
tag = enterpriseSuffix;
72+
}
73+
else if (image.MatchVersion(v => v.Contains(communitySuffix)))
74+
{
75+
tag = image.Tag;
76+
}
77+
else if (image.MatchVersion(v => v.Contains(enterpriseSuffix)))
78+
{
79+
tag = image.Tag;
80+
}
81+
else if (image.MatchVersion(v => operatingSystems.Any(v.Contains)))
82+
{
83+
MatchEvaluator evaluator = match => $"{enterpriseSuffix}-{match.Value}";
84+
tag = Regex.Replace(image.Tag, string.Join("|", operatingSystems), evaluator);
85+
}
86+
else
87+
{
88+
tag = $"{image.Tag}-{enterpriseSuffix}";
89+
}
90+
91+
var enterpriseImage = new DockerImage(image.Repository, image.Registry, tag, tag == null ? image.Digest : null);
92+
93+
var licenseAgreement = acceptLicenseAgreement ? AcceptLicenseAgreement : DeclineLicenseAgreement;
94+
95+
return WithImage(enterpriseImage).WithEnvironment(AcceptLicenseAgreementEnvVar, licenseAgreement);
96+
}
97+
3598
/// <inheritdoc />
3699
public override Neo4jContainer Build()
37100
{
38101
Validate();
39102
return new Neo4jContainer(DockerResourceConfiguration);
40103
}
41104

105+
/// <inheritdoc />
106+
protected override void Validate()
107+
{
108+
const string message = "The image '{0}' requires you to accept a license agreement.";
109+
110+
base.Validate();
111+
112+
Predicate<Neo4jConfiguration> licenseAgreementNotAccepted = value => value.Image.Tag != null && value.Image.Tag.Contains("enterprise")
113+
&& (!value.Environments.TryGetValue(AcceptLicenseAgreementEnvVar, out var licenseAgreementValue) || !AcceptLicenseAgreement.Equals(licenseAgreementValue, StringComparison.Ordinal));
114+
115+
_ = Guard.Argument(DockerResourceConfiguration, nameof(DockerResourceConfiguration.Image))
116+
.ThrowIf(argument => licenseAgreementNotAccepted(argument.Value), argument => throw new ArgumentException(string.Format(message, DockerResourceConfiguration.Image.FullName), argument.Name));
117+
}
118+
42119
/// <inheritdoc />
43120
protected override Neo4jBuilder Init()
44121
{

src/Testcontainers.Neo4j/Usings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
global using System;
2+
global using System.Linq;
3+
global using System.Text.RegularExpressions;
24
global using Docker.DotNet.Models;
5+
global using DotNet.Testcontainers;
36
global using DotNet.Testcontainers.Builders;
47
global using DotNet.Testcontainers.Configurations;
58
global using DotNet.Testcontainers.Containers;
9+
global using DotNet.Testcontainers.Images;
610
global using JetBrains.Annotations;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace Testcontainers.Neo4j;
2+
3+
public sealed class Neo4jBuilderTest
4+
{
5+
[Theory]
6+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
7+
[InlineData("neo4j:5.23.0", "5.23.0-enterprise")]
8+
[InlineData("neo4j:5.23", "5.23-enterprise")]
9+
[InlineData("neo4j:5", "5-enterprise")]
10+
[InlineData("neo4j:5.23.0-community", "5.23.0-community")]
11+
[InlineData("neo4j:5.23-community", "5.23-community")]
12+
[InlineData("neo4j:5-community", "5-community")]
13+
[InlineData("neo4j:community", "community")]
14+
[InlineData("neo4j:5.23.0-bullseye", "5.23.0-enterprise-bullseye")]
15+
[InlineData("neo4j:5.23-bullseye", "5.23-enterprise-bullseye")]
16+
[InlineData("neo4j:5-bullseye", "5-enterprise-bullseye")]
17+
[InlineData("neo4j:bullseye", "enterprise-bullseye")]
18+
[InlineData("neo4j:5.23.0-enterprise-bullseye", "5.23.0-enterprise-bullseye")]
19+
[InlineData("neo4j:5.23-enterprise-bullseye", "5.23-enterprise-bullseye")]
20+
[InlineData("neo4j:5-enterprise-bullseye", "5-enterprise-bullseye")]
21+
[InlineData("neo4j:enterprise-bullseye", "enterprise-bullseye")]
22+
[InlineData("neo4j:5.23.0-enterprise", "5.23.0-enterprise")]
23+
[InlineData("neo4j:5.23-enterprise", "5.23-enterprise")]
24+
[InlineData("neo4j:5-enterprise", "5-enterprise")]
25+
[InlineData("neo4j:enterprise", "enterprise")]
26+
[InlineData("neo4j", "enterprise")]
27+
[InlineData("neo4j@sha256:20eb19e3d60f9f07c12c89eac8d8722e393be7e45c6d7e56004a2c493b8e2032", null)]
28+
public void AppendsEnterpriseSuffixWhenEnterpriseEditionLicenseAgreementIsAccepted(string image, string expected)
29+
{
30+
var neo4jContainer = new Neo4jBuilder().WithImage(image).WithEnterpriseEdition(true).Build();
31+
Assert.Equal(expected, neo4jContainer.Image.Tag);
32+
}
33+
34+
[Theory]
35+
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
36+
[ClassData(typeof(Neo4jBuilderConfigurations))]
37+
public void ThrowsArgumentExceptionWhenEnterpriseEditionLicenseAgreementIsNotAccepted(Neo4jBuilder neo4jBuilder)
38+
{
39+
Assert.Throws<ArgumentException>(neo4jBuilder.Build);
40+
}
41+
42+
private sealed class Neo4jBuilderConfigurations : TheoryData<Neo4jBuilder>
43+
{
44+
public Neo4jBuilderConfigurations()
45+
{
46+
Add(new Neo4jBuilder().WithImage(Neo4jBuilder.Neo4jImage + "-enterprise"));
47+
Add(new Neo4jBuilder().WithEnterpriseEdition(false));
48+
}
49+
}
50+
}
Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
namespace Testcontainers.Neo4j;
22

3-
public sealed class Neo4jContainerTest : IAsyncLifetime
3+
public abstract class Neo4jContainerTest : IAsyncLifetime
44
{
5-
// # --8<-- [start:UseNeo4jContainer]
6-
private readonly Neo4jContainer _neo4jContainer = new Neo4jBuilder().Build();
5+
private readonly Neo4jContainer _neo4jContainer;
6+
7+
private Neo4jContainerTest(Neo4jContainer neo4jContainer)
8+
{
9+
_neo4jContainer = neo4jContainer;
10+
}
11+
12+
public abstract string Edition { get; }
713

14+
// # --8<-- [start:UseNeo4jContainer]
815
public Task InitializeAsync()
916
{
1017
return _neo4jContainer.StartAsync();
@@ -17,18 +24,51 @@ public Task DisposeAsync()
1724

1825
[Fact]
1926
[Trait(nameof(DockerCli.DockerPlatform), nameof(DockerCli.DockerPlatform.Linux))]
20-
public void SessionReturnsDatabase()
27+
public async Task SessionReturnsDatabase()
2128
{
2229
// Given
23-
const string database = "neo4j";
30+
const string neo4jDatabase = "neo4j";
2431

2532
using var driver = GraphDatabase.Driver(_neo4jContainer.GetConnectionString());
2633

2734
// When
28-
using var session = driver.AsyncSession(sessionConfigBuilder => sessionConfigBuilder.WithDatabase(database));
35+
using var session = driver.AsyncSession(sessionConfigBuilder => sessionConfigBuilder.WithDatabase(neo4jDatabase));
36+
37+
var result = await session.RunAsync("CALL dbms.components() YIELD edition RETURN edition")
38+
.ConfigureAwait(true);
39+
40+
var record = await result.SingleAsync()
41+
.ConfigureAwait(true);
42+
43+
var edition = record["edition"].As<string>();
2944

3045
// Then
31-
Assert.Equal(database, session.SessionConfig.Database);
46+
Assert.Equal(neo4jDatabase, session.SessionConfig.Database);
47+
Assert.Equal(Edition, edition);
3248
}
3349
// # --8<-- [end:UseNeo4jContainer]
50+
51+
// # --8<-- [start:CreateNeo4jContainer]
52+
[UsedImplicitly]
53+
public sealed class Neo4jDefaultConfiguration : Neo4jContainerTest
54+
{
55+
public Neo4jDefaultConfiguration()
56+
: base(new Neo4jBuilder().Build())
57+
{
58+
}
59+
60+
public override string Edition => "community";
61+
}
62+
63+
[UsedImplicitly]
64+
public sealed class Neo4jEnterpriseEditionConfiguration : Neo4jContainerTest
65+
{
66+
public Neo4jEnterpriseEditionConfiguration()
67+
: base(new Neo4jBuilder().WithEnterpriseEdition(true).Build())
68+
{
69+
}
70+
71+
public override string Edition => "enterprise";
72+
}
73+
// # --8<-- [end:CreateNeo4jContainer]
3474
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
global using System;
12
global using System.Threading.Tasks;
23
global using DotNet.Testcontainers.Commons;
4+
global using JetBrains.Annotations;
35
global using Neo4j.Driver;
46
global using Xunit;

0 commit comments

Comments
 (0)