diff --git a/docs/modules/databases/neo4j.md b/docs/modules/databases/neo4j.md index 18f7a957ea2..a8570fe7270 100644 --- a/docs/modules/databases/neo4j.md +++ b/docs/modules/databases/neo4j.md @@ -29,7 +29,7 @@ You are not limited to Unit tests, and you can use an instance of the Neo4j Test A custom password can be provided: -[Custom password](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withAdminPassword +[Custom password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withAdminPassword ### Disable authentication @@ -37,7 +37,7 @@ A custom password can be provided: Authentication can be disabled: -[Disable authentication](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withoutAuthentication +[Disable authentication](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withoutAuthentication ### Random password @@ -45,7 +45,7 @@ Authentication can be disabled: A random (`UUID`-random based) password can be set: -[Random password](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:withRandomPassword +[Random password](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:withRandomPassword ### Neo4j-Configuration @@ -54,7 +54,7 @@ Neo4j's Docker image needs Neo4j configuration options in a dedicated format. The container takes care of that, and you can configure the database with standard options like the following: -[Neo4j configuration](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:neo4jConfiguration +[Neo4j configuration](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:neo4jConfiguration ### Add custom plugins @@ -62,13 +62,13 @@ The container takes care of that, and you can configure the database with standa Custom plugins, like APOC, can be copied over to the container from any classpath or host resource like this: -[Plugin jar](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:registerPluginsJar +[Plugin jar](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:registerPluginsJar Whole directories work as well: -[Plugin folder](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:registerPluginsPath +[Plugin folder](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:registerPluginsPath ### Add Neo4j Docker Labs plugins @@ -81,7 +81,7 @@ or [Neo4j 5 plugin list](https://neo4j.com/docs/operations-manual/5/configuratio Please the method `withPlugins(String... plugins)`. -[Configure Neo4j Labs Plugins](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:configureLabsPlugins +[Configure Neo4j Labs Plugins](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:configureLabsPlugins @@ -90,7 +90,7 @@ or [Neo4j 5 plugin list](https://neo4j.com/docs/operations-manual/5/configuratio If you have an existing database (`graph.db`) you want to work with, copy it over to the container like this: -[Copy database](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:copyDatabase +[Copy database](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:copyDatabase !!! note @@ -101,7 +101,7 @@ If you have an existing database (`graph.db`) you want to work with, copy it ove If you need the Neo4j enterprise license, you can declare your Neo4j container like this: -[Enterprise edition](../../../modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java) inside_block:enterpriseEdition +[Enterprise edition](../../../modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java) inside_block:enterpriseEdition This creates a Testcontainers based on the Docker image build with the Enterprise version of Neo4j 4.4. diff --git a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java b/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java index bd3499567d4..64c4df9f350 100644 --- a/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java +++ b/modules/neo4j/src/main/java/org/testcontainers/containers/Neo4jContainer.java @@ -30,7 +30,10 @@ *
  • HTTP: 7474
  • *
  • HTTPS: 7473
  • * + * + * @deprecated use {@link org.testcontainers.neo4j.Neo4jContainer} instead. */ +@Deprecated public class Neo4jContainer extends GenericContainer { /** diff --git a/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java b/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java new file mode 100644 index 00000000000..23a94c98c8f --- /dev/null +++ b/modules/neo4j/src/main/java/org/testcontainers/neo4j/Neo4jContainer.java @@ -0,0 +1,358 @@ +package org.testcontainers.neo4j; + +import org.testcontainers.containers.GenericContainer; +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.ComparableVersion; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.LicenseAcceptance; +import org.testcontainers.utility.MountableFile; + +import java.net.HttpURLConnection; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Testcontainers implementation for Neo4j. + *

    + * Supported image: {@code neo4j} + *

    + * Exposed ports: + *

    + */ +public class Neo4jContainer extends GenericContainer { + + /** + * The image defaults to the official Neo4j image: Neo4j. + */ + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("neo4j"); + + /** + * The default tag (version) to use. + */ + private static final String DEFAULT_TAG = "4.4"; + + private static final String ENTERPRISE_TAG = DEFAULT_TAG + "-enterprise"; + + /** + * Default port for the binary Bolt protocol. + */ + private static final int DEFAULT_BOLT_PORT = 7687; + + /** + * The port of the transactional HTTPS endpoint: Neo4j REST API. + */ + private static final int DEFAULT_HTTPS_PORT = 7473; + + /** + * The port of the transactional HTTP endpoint: Neo4j REST API. + */ + 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 final boolean standardImage; + + private String adminPassword = DEFAULT_ADMIN_PASSWORD; + + private final Set labsPlugins = new HashSet<>(); + + /** + * Default wait strategies + */ + public static final WaitStrategy WAIT_FOR_BOLT = new LogMessageWaitStrategy() + .withRegEx(String.format(".*Bolt enabled on .*:%d\\.\n", DEFAULT_BOLT_PORT)); + + private static final WaitStrategy WAIT_FOR_HTTP = new HttpWaitStrategy() + .forPort(DEFAULT_HTTP_PORT) + .forStatusCodeMatching(response -> response == HttpURLConnection.HTTP_OK); + + /** + * Creates a Neo4jContainer using a specific docker image. + * + * @param dockerImageName The docker image to use. + */ + public Neo4jContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + /** + * Creates a Neo4jContainer using a specific docker image. + * + * @param dockerImageName The docker image to use. + */ + public Neo4jContainer(final DockerImageName dockerImageName) { + super(dockerImageName); + this.standardImage = dockerImageName.getUnversionedPart().equals(DEFAULT_IMAGE_NAME.getUnversionedPart()); + + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + this.waitStrategy = + new WaitAllStrategy() + .withStrategy(WAIT_FOR_BOLT) + .withStrategy(WAIT_FOR_HTTP) + .withStartupTimeout(Duration.ofMinutes(2)); + + addExposedPorts(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT); + } + + @Override + public Set getLivenessCheckPortNumbers() { + return Stream + .of(DEFAULT_BOLT_PORT, DEFAULT_HTTP_PORT, DEFAULT_HTTPS_PORT) + .map(this::getMappedPort) + .collect(Collectors.toSet()); + } + + @Override + protected void configure() { + configureAuth(); + configureLabsPlugins(); + configureWaitStrategy(); + } + + /** + * Configured via {@link Neo4jContainer#withAdminPassword(String)} or {@link Neo4jContainer#withoutAuthentication()} + * It is only possible to set the correct auth in the configuration call. + * Also, the custom methods overrule the set env parameter. + */ + private void configureAuth() { + String neo4jAuthEnvKey = "NEO4J_AUTH"; + if (!getEnvMap().containsKey(neo4jAuthEnvKey) || !DEFAULT_ADMIN_PASSWORD.equals(this.adminPassword)) { + boolean emptyAdminPassword = this.adminPassword == null || this.adminPassword.isEmpty(); + String neo4jAuth = emptyAdminPassword ? "none" : String.format(AUTH_FORMAT, this.adminPassword); + addEnv(neo4jAuthEnvKey, neo4jAuth); + } + } + + /** + * Configured via {@link Neo4jContainer#withLabsPlugins}. + * Configuration can only happen in the configuration call because there is no default. + */ + private void configureLabsPlugins() { + String neo4jLabsPluginsEnvKey = "NEO4JLABS_PLUGINS"; + if (!getEnv().contains(neo4jLabsPluginsEnvKey) && !this.labsPlugins.isEmpty()) { + String enabledPlugins = + this.labsPlugins.stream().map(pluginName -> "\"" + pluginName + "\"").collect(Collectors.joining(",")); + + addEnv(neo4jLabsPluginsEnvKey, "[" + enabledPlugins + "]"); + } + } + + /** + * Update the default Neo4jContainer wait strategy based on the exposed ports. + * Still possible to override the startup timeout before starting the container via {@link WaitStrategy#withStartupTimeout(Duration)}. + */ + private void configureWaitStrategy() { + List exposedPorts = getExposedPorts(); + boolean boltExposed = exposedPorts.contains(DEFAULT_BOLT_PORT); + boolean httpExposed = exposedPorts.contains(DEFAULT_HTTP_PORT); + boolean onlyBoltExposed = boltExposed && !httpExposed; + boolean onlyHttpExposed = !boltExposed && httpExposed; + + if (onlyBoltExposed) { + this.waitStrategy = + new WaitAllStrategy().withStrategy(WAIT_FOR_BOLT).withStartupTimeout(Duration.ofMinutes(2)); + } else if (onlyHttpExposed) { + this.waitStrategy = + new WaitAllStrategy().withStrategy(WAIT_FOR_HTTP).withStartupTimeout(Duration.ofMinutes(2)); + } + } + + /** + * @return Bolt URL for use with Neo4j's Java-Driver. + */ + public String getBoltUrl() { + return String.format("bolt://" + getHost() + ":" + getMappedPort(DEFAULT_BOLT_PORT)); + } + + /** + * @return URL of the transactional HTTP endpoint. + */ + public String getHttpUrl() { + return String.format("http://" + getHost() + ":" + getMappedPort(DEFAULT_HTTP_PORT)); + } + + /** + * @return URL of the transactional HTTPS endpoint. + */ + public String getHttpsUrl() { + return String.format("https://" + getHost() + ":" + getMappedPort(DEFAULT_HTTPS_PORT)); + } + + /** + * Configures the container to use the enterprise edition of the default docker image. + *

    + * Please have a look at the Neo4j Licensing page. 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 Neo4jContainer withEnterpriseEdition() { + if (!standardImage) { + throw new IllegalStateException( + String.format("Cannot use enterprise version with alternative image %s.", getDockerImageName()) + ); + } + + setDockerImageName(DEFAULT_IMAGE_NAME.withTag(ENTERPRISE_TAG).asCanonicalNameString()); + LicenseAcceptance.assertLicenseAccepted(getDockerImageName()); + + addEnv("NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes"); + + return self(); + } + + /** + * Sets the admin password for the default account (which is
    neo4j
    ). A null value or an empty string + * disables authentication. + * + * @param adminPassword The admin password for the default database account. + * @return This container. + */ + public Neo4jContainer withAdminPassword(final String adminPassword) { + if (adminPassword != null && adminPassword.length() < 8) { + logger().warn("Your provided admin password is too short and will not work with Neo4j 5.3+."); + } + this.adminPassword = adminPassword; + return self(); + } + + /** + * Disables authentication. + * + * @return This container. + */ + public Neo4jContainer withoutAuthentication() { + return withAdminPassword(null); + } + + /** + * Copies an existing {@code graph.db} folder into the container. This can either be a classpath resource or a + * host resource. Please have a look at the factory methods in {@link MountableFile}. + *
    + * If you want to map your database into the container instead of copying them, please use {@code #withClasspathResourceMapping}, + * but this will only work when your test does not run in a container itself. + *
    + * Note: This method only works with Neo4j 3.5. + *
    + * Mapping would work like this: + *
    +     *      @Container
    +     *      private static final Neo4jContainer databaseServer = new Neo4jContainer<>()
    +     *          .withClasspathResourceMapping("/test-graph.db", "/data/databases/graph.db", BindMode.READ_WRITE);
    +     * 
    + * + * @param graphDb The graph.db folder to copy into the container + * @throws IllegalArgumentException If the database version is not 3.5. + * @return This container. + */ + public Neo4jContainer withDatabase(MountableFile graphDb) { + if (!isNeo4jDatabaseVersionSupportingDbCopy()) { + throw new IllegalArgumentException( + "Copying database folder is not supported for Neo4j instances with version 4.0 or higher." + ); + } + return withCopyFileToContainer(graphDb, "/data/databases/graph.db"); + } + + /** + * Adds plugins to the given directory to the container. If {@code plugins} denotes a directory, than all of that + * directory is mapped to Neo4j's plugins. Otherwise, single resources are copied over. + *
    + * If you want to map your plugins into the container instead of copying them, please use {@code #withClasspathResourceMapping}, + * but this will only work when your test does not run in a container itself. + * + * @param plugins + * @return This container. + */ + public Neo4jContainer withPlugins(MountableFile plugins) { + return withCopyFileToContainer(plugins, "/var/lib/neo4j/plugins/"); + } + + /** + * Adds Neo4j configuration properties to the container. The properties can be added as in the official Neo4j + * configuration, the method automatically translate them into the format required by the Neo4j container. + * + * @param key The key to configure, i.e. {@code dbms.security.procedures.unrestricted} + * @param value The value to set + * @return This container. + */ + public Neo4jContainer withNeo4jConfig(String key, String value) { + addEnv(formatConfigurationKey(key), value); + return self(); + } + + /** + * @return The admin password for the neo4j account or literal null if auth is disabled. + */ + public String getAdminPassword() { + return adminPassword; + } + + /** + * Registers one or more Neo4j plugins for server startup. + * The plugins are listed here + * + * + * @param plugins The Neo4j plugins that should get started with the server. + * @return This container. + */ + public Neo4jContainer withPlugins(String... plugins) { + this.labsPlugins.addAll(Arrays.asList(plugins)); + return self(); + } + + private static String formatConfigurationKey(String plainConfigKey) { + final String prefix = "NEO4J_"; + + return String.format("%s%s", prefix, plainConfigKey.replaceAll("_", "__").replaceAll("\\.", "_")); + } + + private boolean isNeo4jDatabaseVersionSupportingDbCopy() { + String usedImageVersion = DockerImageName.parse(getDockerImageName()).getVersionPart(); + ComparableVersion usedComparableVersion = new ComparableVersion(usedImageVersion); + + boolean versionSupportingDbCopy = + usedComparableVersion.isLessThan("4.0") && usedComparableVersion.isGreaterThanOrEqualTo("2"); + + if (versionSupportingDbCopy) { + return true; + } + if (!usedComparableVersion.isSemanticVersion()) { + logger() + .warn( + "Version {} is not a semantic version. The function \"withDatabase\" will fail.", + usedImageVersion + ); + logger().warn("Copying databases is only supported for Neo4j versions 3.5.x"); + } + + return false; + } + + public Neo4jContainer withRandomPassword() { + return withAdminPassword(UUID.randomUUID().toString()); + } +} diff --git a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java b/modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java similarity index 98% rename from modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java rename to modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java index 997e90c4788..82e8bc16e08 100644 --- a/modules/neo4j/src/test/java/org/testcontainers/containers/Neo4jContainerTest.java +++ b/modules/neo4j/src/test/java/org/testcontainers/neo4j/Neo4jContainerTest.java @@ -1,4 +1,4 @@ -package org.testcontainers.containers; +package org.testcontainers.neo4j; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; @@ -263,7 +263,7 @@ void shouldRespectCustomWaitStrategy() { neo4jContainer.configure(); - assertThat(neo4jContainer.getWaitStrategy()).isInstanceOf(CustomDummyWaitStrategy.class); + assertThat(neo4jContainer).extracting("waitStrategy").isInstanceOf(CustomDummyWaitStrategy.class); } @Test