Skip to content

Provide a Testcontainer for Neo4j. #993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 8, 2018
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
* [Recording videos](usage/webdriver_containers.md#recording-videos)

* [Kafka containers](usage/kafka_containers.md)
* [Neo4j containers](usage/neo4j_container.md)
* [Docker Compose](usage/docker_compose.md)
* [Dockerfile containers](usage/dockerfile.md)
* [Windows support](usage/windows_support.md)
Expand Down
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Testcontainers will try to connect to a Docker daemon using the following strate
* [Elasticsearch container](usage/elasticsearch_container.md) - Elasticsearch container support
* [Webdriver containers](usage/webdriver_containers.md) - run a dockerized Chrome or Firefox browser ready for Selenium/Webdriver operations - complete with automatic video recording
* [Kafka containers](usage/kafka_containers.md) - run a dockerized Kafka, a distributed streaming platform
* [Neo4j container](usage/neo4j_container.md) - Neo4j container support
* [Generic containers](usage/generic_containers.md) - run any Docker container as a test dependency
* [Docker compose](usage/docker_compose.md) - reuse services defined in a Docker Compose YAML file
* [Dockerfile containers](usage/dockerfile.md) - run a container that is built on-the-fly from a Dockerfile
Expand Down
102 changes: 102 additions & 0 deletions docs/usage/neo4j_container.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Neo4j container

This module helps running [Neo4j](https://neo4j.com/download/) using Testcontainers.

Note that it's based on the [official Docker image](https://hub.docker.com/_/neo4j/) provided by Neo4j, Inc.

## Dependencies

Add the Neo4j Testcontainer module:

```groovy
testCompile "org.testcontainers:neo4j"
```

and the Neo4j Java driver if you plan to access the Testcontainer via Bolt:

```groovy
compile "org.neo4j.driver:neo4j-java-driver:1.7.1"
```

## Usage example

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.
You can either use call `getHttpUrl()` or `getBoltUrl()` on the Neo4j container.
`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/).
On the JVM you would most likely use the [Java driver](https://github.com/neo4j/neo4j-java-driver).

The following example uses the JUnit 5 extension `@Testcontainers` and demonstrates both the usage of the Java Driver and the REST endpoint:

```java
@Testcontainers
public class ExampleTest {

@Container
private static Neo4jContainer neo4jContainer = new Neo4jContainer()
.withAdminPassword(null); // Disable password

@Test
void testSomethingUsingBolt() {

// Retrieve the Bolt URL from the container
String boltUrl = neo4jContainer.getBoltUrl();
try (
Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none());
Session session = driver.session()
) {
long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
assertThat(one, is(1L));
} catch (Exception e) {
fail(e.getMessage());
}
}

@Test
void testSomethingUsingHttp() throws IOException {

// Retrieve the HTTP URL from the container
String httpUrl = neo4jContainer.getHttpUrl();

URL url = new URL(httpUrl + "/db/data/transaction/commit");
HttpURLConnection con = (HttpURLConnection) url.openConnection();

con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "application/json");
con.setDoOutput(true);

try (Writer out = new OutputStreamWriter(con.getOutputStream())) {
out.write("{\"statements\":[{\"statement\":\"RETURN 1\"}]}");
out.flush();
}

assertThat(con.getResponseCode(), is(HttpURLConnection.HTTP_OK));
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
String expectedResponse =
"{\"results\":[{\"columns\":[\"1\"],\"data\":[{\"row\":[1],\"meta\":[null]}]}],\"errors\":[]}";
String response = buffer.lines().collect(Collectors.joining("\n"));
assertThat(response, is(expectedResponse));
}
}
}
```

You are not limited to Unit tests and can of course use an instance of the Neo4j Testcontainer in vanilla Java code as well.


## Choose your Neo4j license

If you need the Neo4j enterprise license, you can declare your Neo4j container like this:

```java
@Testcontainers
public class ExampleTest {
@ClassRule
public static Neo4jContainer neo4jContainer = new Neo4jContainer()
.withEnterpriseEdition();
}
```

This creates a Testcontainer based on the Docker image build with the Enterprise version of Neo4j.
The call to `withEnterpriseEdition` adds the required environment variable that you accepted the terms and condition of the enterprise version.
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.
You'll find more information about licensing Neo4j here: [About Neo4j Licenses](https://neo4j.com/licensing/).
1 change: 1 addition & 0 deletions modules/neo4j/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
container-license-acceptance.txt
8 changes: 8 additions & 0 deletions modules/neo4j/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description = "TestContainers :: Neo4j"

dependencies {
compile project(":testcontainers")

testCompile "org.neo4j.driver:neo4j-java-driver:1.7.1"
testCompile 'org.assertj:assertj-core:3.11.1'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package org.testcontainers.containers;

import static java.net.HttpURLConnection.*;
import static java.util.stream.Collectors.*;

import java.time.Duration;
import java.util.Set;
import java.util.stream.Stream;

import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.LicenseAcceptance;

/**
* Testcontainer for Neo4j.
*
* @param <S> "SELF" to be used in the <code>withXXX</code> methods.
* @author Michael J. Simons
*/
public final class Neo4jContainer<S extends Neo4jContainer<S>> extends GenericContainer<S> {

/**
* The image defaults to the official Neo4j image: <a href="https://hub.docker.com/_/neo4j/">Neo4j</a>.
*/
private static final String DEFAULT_IMAGE_NAME = "neo4j";

/**
* The default tag (version) to use.
*/
private static final String DEFAULT_TAG = "3.5.0";

private static final String DOCKER_IMAGE_NAME = DEFAULT_IMAGE_NAME + ":" + DEFAULT_TAG;

/**
* Default port for the binary Bolt protocol.
*/
private static final int DEFAULT_BOLT_PORT = 7687;

/**
* The port of the transactional HTTPS endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
*/
private static final int DEFAULT_HTTPS_PORT = 7473;

/**
* The port of the transactional HTTP endpoint: <a href="https://neo4j.com/docs/rest-docs/current/">Neo4j REST API</a>.
*/
private static final int DEFAULT_HTTP_PORT = 7474;

/**
* The official image requires a change of password by default from "neo4j" to something else. This defaults to "password".
*/
private static final String DEFAULT_ADMIN_PASSWORD = "password";

private static final String AUTH_FORMAT = "neo4j/%s";

private String adminPassword = DEFAULT_ADMIN_PASSWORD;

private boolean defaultImage = false;

/**
* Creates a Testcontainer using the official Neo4j docker image.
*/
public Neo4jContainer() {
this(DOCKER_IMAGE_NAME);

this.defaultImage = true;
}

/**
* Creates a Testcontainer using a specific docker image.
*
* @param dockerImageName The docker image to use.
*/
public Neo4jContainer(String dockerImageName) {
super(dockerImageName);

WaitStrategy waitForBolt = new LogMessageWaitStrategy()
.withRegEx(String.format(".*Bolt enabled on 0\\.0\\.0\\.0:%d\\.\n", DEFAULT_BOLT_PORT));
WaitStrategy waitForHttp = new HttpWaitStrategy()
.forPort(DEFAULT_HTTP_PORT)
.forStatusCodeMatching(response -> response == HTTP_OK);

this.waitStrategy = new WaitAllStrategy()
.withStrategy(waitForBolt)
.withStrategy(waitForHttp)
.withStartupTimeout(Duration.ofMinutes(2));
}

@Override
public Set<Integer> getLivenessCheckPortNumbers() {

return Stream.of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT)
.map(this::getMappedPort)
.collect(toSet());
}

@Override
protected void configure() {

addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT);

boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty();
String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword);
addEnv("NEO4J_AUTH", neo4jAuth);
}

/**
* @return Bolt URL for use with Neo4j's Java-Driver.
*/
public String getBoltUrl() {
return String.format("bolt://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_BOLT_PORT));
}

/**
* @return URL of the transactional HTTP endpoint.
*/
public String getHttpUrl() {
return String.format("http://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_HTTP_PORT));
}

/**
* @return URL of the transactional HTTPS endpoint.
*/
public String getHttpsUrl() {
return String.format("https://" + getContainerIpAddress() + ":" + getMappedPort(DEFAULT_HTTPS_PORT));
}

/**
* Configures the container to use the enterprise edition of the default docker image.
* <br><br>
* Please have a look at the <a href="https://neo4j.com/licensing/">Neo4j Licensing page</a>. While the Neo4j
* Community Edition can be used for free in your projects under the GPL v3 license, Neo4j Enterprise edition
* needs either a commercial, education or evaluation license.
*
* @return This container.
*/
public S withEnterpriseEdition() {

if (!defaultImage) {
throw new IllegalStateException(
String.format("Cannot use enterprise version with alternative image %s.", getDockerImageName()));
}

setDockerImageName(DOCKER_IMAGE_NAME + "-enterprise");
LicenseAcceptance.assertLicenseAccepted(getDockerImageName());

addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes");

return self();
}

/**
* Sets the admin password for the default account (which is <pre>neo4j</pre>). A null value or an empty string
* disables authentication.
*
* @param adminPassword The admin password for the default database account.
* @return This container.
*/
public S withAdminPassword(final String adminPassword) {

this.adminPassword = adminPassword;
return self();
}

/**
* @return The admin password for the <code>neo4j</code> account or literal <code>null</code> if auth is disabled.
*/
public String getAdminPassword() {
return adminPassword;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.testcontainers.containers;

import static org.assertj.core.api.Assertions.*;

import java.util.Collections;

import org.junit.ClassRule;
import org.junit.Test;
import org.neo4j.driver.v1.AuthTokens;
import org.neo4j.driver.v1.Driver;
import org.neo4j.driver.v1.GraphDatabase;
import org.neo4j.driver.v1.Session;

/**
* Test for basic functionality when used as a <code>@ClassRule</code>.
*
* @author Michael J. Simons
*/
public class Neo4jContainerJUnitIntegrationTest {

@ClassRule
public static Neo4jContainer neo4jContainer = new Neo4jContainer();

@Test
public void shouldStart() {

boolean actual = neo4jContainer.isRunning();
assertThat(actual).isTrue();

try (Driver driver = GraphDatabase
.driver(neo4jContainer.getBoltUrl(), AuthTokens.basic("neo4j", "password"));
Session session = driver.session()
) {
long one = session.run("RETURN 1", Collections.emptyMap()).next().get(0).asLong();
assertThat(one).isEqualTo(1L);
} catch (Exception e) {
fail(e.getMessage());
}
}

@Test
public void shouldReturnBoltUrl() {
String actual = neo4jContainer.getBoltUrl();

assertThat(actual).isNotNull();
assertThat(actual).startsWith("bolt://");
}

@Test
public void shouldReturnHttpUrl() {
String actual = neo4jContainer.getHttpUrl();

assertThat(actual).isNotNull();
assertThat(actual).startsWith("http://");
}
}
Loading