Skip to content

Commit e435dd4

Browse files
michael-simonsrnorth
authored andcommitted
Provide a Testcontainer for Neo4j. (#993)
I'd like to propose a Testcontainer for Neo4j, an open source graph database as in this pull request. The test container is build with our official Docker image and provides the following features to the user: * To retrieve a Bolt url for usage with one of the official drivers, most likely the Java driver * To retrieve HTTP and HTTPS urls for the transactional REST api * Especially to disable authentication or chose a password * Also explicitly use the enterprise version of Neo4j While we offer a test harness and users may opt to use an embedded version of our database during test, it seems that many people think it's a better practice to test any database access against "the real thing". The container would be useful to us and allegedly to some other projects using Neo4j as well. We have been using a container very similar while spiking our efforts of a [reactive client for Neo4j](https://github.com/michael-simons/neo4j-reactive-java-client).
1 parent dce6264 commit e435dd4

File tree

9 files changed

+425
-0
lines changed

9 files changed

+425
-0
lines changed

docs/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
* [Recording videos](usage/webdriver_containers.md#recording-videos)
4848

4949
* [Kafka containers](usage/kafka_containers.md)
50+
* [Neo4j containers](usage/neo4j_container.md)
5051
* [Docker Compose](usage/docker_compose.md)
5152
* [Dockerfile containers](usage/dockerfile.md)
5253
* [Windows support](usage/windows_support.md)

docs/usage.md

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Testcontainers will try to connect to a Docker daemon using the following strate
2727
* [Elasticsearch container](usage/elasticsearch_container.md) - Elasticsearch container support
2828
* [Webdriver containers](usage/webdriver_containers.md) - run a dockerized Chrome or Firefox browser ready for Selenium/Webdriver operations - complete with automatic video recording
2929
* [Kafka containers](usage/kafka_containers.md) - run a dockerized Kafka, a distributed streaming platform
30+
* [Neo4j container](usage/neo4j_container.md) - Neo4j container support
3031
* [Generic containers](usage/generic_containers.md) - run any Docker container as a test dependency
3132
* [Docker compose](usage/docker_compose.md) - reuse services defined in a Docker Compose YAML file
3233
* [Dockerfile containers](usage/dockerfile.md) - run a container that is built on-the-fly from a Dockerfile

docs/usage/neo4j_container.md

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Neo4j container
2+
3+
This module helps running [Neo4j](https://neo4j.com/download/) using Testcontainers.
4+
5+
Note that it's based on the [official Docker image](https://hub.docker.com/_/neo4j/) provided by Neo4j, Inc.
6+
7+
## Dependencies
8+
9+
Add the Neo4j Testcontainer module:
10+
11+
```groovy
12+
testCompile "org.testcontainers:neo4j"
13+
```
14+
15+
and the Neo4j Java driver if you plan to access the Testcontainer via Bolt:
16+
17+
```groovy
18+
compile "org.neo4j.driver:neo4j-java-driver:1.7.1"
19+
```
20+
21+
## Usage example
22+
23+
Declare your Testcontainer as a `@ClassRule` or `@Rule` in a JUnit 4 test or as static or member attribute of a JUnit 5 test annotated with `@Container` as you would with other Testcontainers.
24+
You can either use call `getHttpUrl()` or `getBoltUrl()` on the Neo4j container.
25+
`getHttpUrl()` gives you the HTTP-address of the transactional HTTP endpoint while `getBoltUrl()` is meant to be used with one of the [official Bolt drivers](https://neo4j.com/docs/developer-manual/preview/drivers/).
26+
On the JVM you would most likely use the [Java driver](https://github.com/neo4j/neo4j-java-driver).
27+
28+
The following example uses the JUnit 5 extension `@Testcontainers` and demonstrates both the usage of the Java Driver and the REST endpoint:
29+
30+
```java
31+
@Testcontainers
32+
public class ExampleTest {
33+
34+
@Container
35+
private static Neo4jContainer neo4jContainer = new Neo4jContainer()
36+
.withAdminPassword(null); // Disable password
37+
38+
@Test
39+
void testSomethingUsingBolt() {
40+
41+
// Retrieve the Bolt URL from the container
42+
String boltUrl = neo4jContainer.getBoltUrl();
43+
try (
44+
Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none());
45+
Session session = driver.session()
46+
) {
47+
long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
48+
assertThat(one, is(1L));
49+
} catch (Exception e) {
50+
fail(e.getMessage());
51+
}
52+
}
53+
54+
@Test
55+
void testSomethingUsingHttp() throws IOException {
56+
57+
// Retrieve the HTTP URL from the container
58+
String httpUrl = neo4jContainer.getHttpUrl();
59+
60+
URL url = new URL(httpUrl + "/db/data/transaction/commit");
61+
HttpURLConnection con = (HttpURLConnection) url.openConnection();
62+
63+
con.setRequestMethod("POST");
64+
con.setRequestProperty("Content-Type", "application/json");
65+
con.setDoOutput(true);
66+
67+
try (Writer out = new OutputStreamWriter(con.getOutputStream())) {
68+
out.write("{\"statements\":[{\"statement\":\"RETURN 1\"}]}");
69+
out.flush();
70+
}
71+
72+
assertThat(con.getResponseCode(), is(HttpURLConnection.HTTP_OK));
73+
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
74+
String expectedResponse =
75+
"{\"results\":[{\"columns\":[\"1\"],\"data\":[{\"row\":[1],\"meta\":[null]}]}],\"errors\":[]}";
76+
String response = buffer.lines().collect(Collectors.joining("\n"));
77+
assertThat(response, is(expectedResponse));
78+
}
79+
}
80+
}
81+
```
82+
83+
You are not limited to Unit tests and can of course use an instance of the Neo4j Testcontainer in vanilla Java code as well.
84+
85+
86+
## Choose your Neo4j license
87+
88+
If you need the Neo4j enterprise license, you can declare your Neo4j container like this:
89+
90+
```java
91+
@Testcontainers
92+
public class ExampleTest {
93+
@ClassRule
94+
public static Neo4jContainer neo4jContainer = new Neo4jContainer()
95+
.withEnterpriseEdition();
96+
}
97+
```
98+
99+
This creates a Testcontainer based on the Docker image build with the Enterprise version of Neo4j.
100+
The call to `withEnterpriseEdition` adds the required environment variable that you accepted the terms and condition of the enterprise version.
101+
You accept those by adding a file named `container-license-acceptance.txt` to the root of your classpath containing the text `neo4j:3.5.0-enterprise` in one line.
102+
You'll find more information about licensing Neo4j here: [About Neo4j Licenses](https://neo4j.com/licensing/).

modules/neo4j/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
container-license-acceptance.txt

modules/neo4j/build.gradle

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
description = "TestContainers :: Neo4j"
2+
3+
dependencies {
4+
compile project(":testcontainers")
5+
6+
testCompile "org.neo4j.driver:neo4j-java-driver:1.7.1"
7+
testCompile 'org.assertj:assertj-core:3.11.1'
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package org.testcontainers.containers;
2+
3+
import static java.net.HttpURLConnection.*;
4+
import static java.util.stream.Collectors.*;
5+
6+
import java.time.Duration;
7+
import java.util.Set;
8+
import java.util.stream.Stream;
9+
10+
import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
11+
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
12+
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
13+
import org.testcontainers.containers.wait.strategy.WaitStrategy;
14+
import org.testcontainers.utility.LicenseAcceptance;
15+
16+
/**
17+
* Testcontainer for Neo4j.
18+
*
19+
* @param <S> "SELF" to be used in the <code>withXXX</code> methods.
20+
* @author Michael J. Simons
21+
*/
22+
public final class Neo4jContainer<S extends Neo4jContainer<S>> extends GenericContainer<S> {
23+
24+
/**
25+
* The image defaults to the official Neo4j image: <a href="https://hub.docker.com/_/neo4j/">Neo4j</a>.
26+
*/
27+
private static final String DEFAULT_IMAGE_NAME = "neo4j";
28+
29+
/**
30+
* The default tag (version) to use.
31+
*/
32+
private static final String DEFAULT_TAG = "3.5.0";
33+
34+
private static final String DOCKER_IMAGE_NAME = DEFAULT_IMAGE_NAME + ":" + DEFAULT_TAG;
35+
36+
/**
37+
* Default port for the binary Bolt protocol.
38+
*/
39+
private static final int DEFAULT_BOLT_PORT = 7687;
40+
41+
/**
42+
* The port of the transactional HTTPS endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
43+
*/
44+
private static final int DEFAULT_HTTPS_PORT = 7473;
45+
46+
/**
47+
* The port of the transactional HTTP endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
48+
*/
49+
private static final int DEFAULT_HTTP_PORT = 7474;
50+
51+
/**
52+
* The official image requires a change of password by default from "neo4j" to something else. This defaults to "password".
53+
*/
54+
private static final String DEFAULT_ADMIN_PASSWORD = "password";
55+
56+
private static final String AUTH_FORMAT = "neo4j/%s";
57+
58+
private String adminPassword = DEFAULT_ADMIN_PASSWORD;
59+
60+
private boolean defaultImage = false;
61+
62+
/**
63+
* Creates a Testcontainer using the official Neo4j docker image.
64+
*/
65+
public Neo4jContainer() {
66+
this(DOCKER_IMAGE_NAME);
67+
68+
this.defaultImage = true;
69+
}
70+
71+
/**
72+
* Creates a Testcontainer using a specific docker image.
73+
*
74+
* @param dockerImageName The docker image to use.
75+
*/
76+
public Neo4jContainer(String dockerImageName) {
77+
super(dockerImageName);
78+
79+
WaitStrategy waitForBolt = new LogMessageWaitStrategy()
80+
.withRegEx(String.format(".*Bolt enabled on 0\\.0\\.0\\.0:%d\\.\n", DEFAULT_BOLT_PORT));
81+
WaitStrategy waitForHttp = new HttpWaitStrategy()
82+
.forPort(DEFAULT_HTTP_PORT)
83+
.forStatusCodeMatching(response -> response == HTTP_OK);
84+
85+
this.waitStrategy = new WaitAllStrategy()
86+
.withStrategy(waitForBolt)
87+
.withStrategy(waitForHttp)
88+
.withStartupTimeout(Duration.ofMinutes(2));
89+
}
90+
91+
@Override
92+
public Set<Integer> getLivenessCheckPortNumbers() {
93+
94+
return Stream.of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT)
95+
.map(this::getMappedPort)
96+
.collect(toSet());
97+
}
98+
99+
@Override
100+
protected void configure() {
101+
102+
addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT);
103+
104+
boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty();
105+
String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword);
106+
addEnv("NEO4J_AUTH", neo4jAuth);
107+
}
108+
109+
/**
110+
* @return Bolt URL for use with Neo4j's Java-Driver.
111+
*/
112+
public String getBoltUrl() {
113+
return String.format("bolt://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_BOLT_PORT));
114+
}
115+
116+
/**
117+
* @return URL of the transactional HTTP endpoint.
118+
*/
119+
public String getHttpUrl() {
120+
return String.format("http://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_HTTP_PORT));
121+
}
122+
123+
/**
124+
* @return URL of the transactional HTTPS endpoint.
125+
*/
126+
public String getHttpsUrl() {
127+
return String.format("https://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_HTTPS_PORT));
128+
}
129+
130+
/**
131+
* Configures the container to use the enterprise edition of the default docker image.
132+
* <br><br>
133+
* Please have a look at the <a href="https://neo4j.com/licensing/">Neo4j Licensing page</a>. While the Neo4j
134+
* Community Edition can be used for free in your projects under the GPL v3 license, Neo4j Enterprise edition
135+
* needs either a commercial, education or evaluation license.
136+
*
137+
* @return This container.
138+
*/
139+
public S withEnterpriseEdition() {
140+
141+
if (!defaultImage) {
142+
throw new IllegalStateException(
143+
String.format("Cannot use enterprise version with alternative image %s.", getDockerImageName()));
144+
}
145+
146+
setDockerImageName(DOCKER_IMAGE_NAME + "-enterprise");
147+
LicenseAcceptance.assertLicenseAccepted(getDockerImageName());
148+
149+
addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes");
150+
151+
return self();
152+
}
153+
154+
/**
155+
* Sets the admin password for the default account (which is <pre>neo4j</pre>). A null value or an empty string
156+
* disables authentication.
157+
*
158+
* @param adminPassword The admin password for the default database account.
159+
* @return This container.
160+
*/
161+
public S withAdminPassword(final String adminPassword) {
162+
163+
this.adminPassword = adminPassword;
164+
return self();
165+
}
166+
167+
/**
168+
* @return The admin password for the <code>neo4j</code> account or literal <code>null</code> if auth is disabled.
169+
*/
170+
public String getAdminPassword() {
171+
return adminPassword;
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.testcontainers.containers;
2+
3+
import static org.assertj.core.api.Assertions.*;
4+
5+
import java.util.Collections;
6+
7+
import org.junit.ClassRule;
8+
import org.junit.Test;
9+
import org.neo4j.driver.v1.AuthTokens;
10+
import org.neo4j.driver.v1.Driver;
11+
import org.neo4j.driver.v1.GraphDatabase;
12+
import org.neo4j.driver.v1.Session;
13+
14+
/**
15+
* Test for basic functionality when used as a <code>@ClassRule</code>.
16+
*
17+
* @author Michael J. Simons
18+
*/
19+
public class Neo4jContainerJUnitIntegrationTest {
20+
21+
@ClassRule
22+
public static Neo4jContainer neo4jContainer = new Neo4jContainer();
23+
24+
@Test
25+
public void shouldStart() {
26+
27+
boolean actual = neo4jContainer.isRunning();
28+
assertThat(actual).isTrue();
29+
30+
try (Driver driver = GraphDatabase
31+
.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", "password"));
32+
Session session = driver.session()
33+
) {
34+
long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
35+
assertThat(one).isEqualTo(1L);
36+
} catch (Exception e) {
37+
fail(e.getMessage());
38+
}
39+
}
40+
41+
@Test
42+
public void shouldReturnBoltUrl() {
43+
String actual = neo4jContainer.getBoltUrl();
44+
45+
assertThat(actual).isNotNull();
46+
assertThat(actual).startsWith("bolt://");
47+
}
48+
49+
@Test
50+
public void shouldReturnHttpUrl() {
51+
String actual = neo4jContainer.getHttpUrl();
52+
53+
assertThat(actual).isNotNull();
54+
assertThat(actual).startsWith("http://");
55+
}
56+
}

0 commit comments

Comments
 (0)