diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7b8c908a..620aa022 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -172,7 +172,7 @@ mvn test -Dimage=$(cat build//coredb/.image-id-community) -Dadminimage= 1. Select the "EnvFile" tab 2. Make sure "Enable EnvFile" is checked. 3. Click the `+` then click to add a `.env` file. - 4. In the file selection box select `./build//devenv-enterprise.env` or `./build//devenv-community.env` depending on which one you want to test. If you do not have the `./tmp` directory, build the docker image and it will be created. + 4. In the file selection box select `./build//devenv-enterprise.env` or `./build//devenv-community.env` depending on which one you want to test. If you do not have the `./build` directory, build the docker image and it will be created. 5. Rebuilding the Neo4j image will regenerate the `.env` files, so you don't need to worry about keeping the environment up to date. You should now be able to run unit tests straight from the IDE. diff --git a/build-docker-image.sh b/build-docker-image.sh index 7e343b97..2c852461 100755 --- a/build-docker-image.sh +++ b/build-docker-image.sh @@ -75,8 +75,6 @@ function get_compatible_dockerfile_for_os_or_error fi echo >&2 "${IMAGE_OS} is not a supported operating system for ${version}." usage - DOCKERFILE_NAME - } function tarball_name @@ -167,12 +165,10 @@ cp "$(cached_tarball "${NEO4JVERSION}" "${NEO4JEDITION}")" ${COREDB_LOCALCXT_DIR # create coredb Dockerfile cp "${SRC_DIR}/${SERIES}/coredb/${DOCKERFILE_NAME}" "${COREDB_LOCALCXT_DIR}/Dockerfile" -sed -i \ - -e "s|%%NEO4J_SHA%%|${coredb_sha}|" \ - -e "s|%%NEO4J_TARBALL%%|$(tarball_name "${NEO4JVERSION}" "${NEO4JEDITION}")|" \ - -e "s|%%NEO4J_EDITION%%|${NEO4JEDITION}|" \ - -e "s|%%NEO4J_DIST_SITE%%|${DISTRIBUTION_SITE}|" \ - "${COREDB_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_SHA%%|${coredb_sha}|" "${COREDB_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_TARBALL%%|$(tarball_name "${NEO4JVERSION}" "${NEO4JEDITION}")|" "${COREDB_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_EDITION%%|${NEO4JEDITION}|" "${COREDB_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_DIST_SITE%%|${DISTRIBUTION_SITE}|" "${COREDB_LOCALCXT_DIR}/Dockerfile" # copy neo4j-admin sources mkdir -p ${ADMIN_LOCALCXT_DIR}/local-package @@ -182,13 +178,10 @@ cp ${SRC_DIR}/${SERIES}/neo4j-admin/*.sh ${ADMIN_LOCALCXT_DIR}/local-package # create neo4j-admin Dockerfile cp "${SRC_DIR}/${SERIES}/neo4j-admin/${DOCKERFILE_NAME}" "${ADMIN_LOCALCXT_DIR}/Dockerfile" -sed -i \ - -e "s|%%NEO4J_SHA%%|${coredb_sha}|" \ - -e "s|%%NEO4J_TARBALL%%|$(tarball_name ${NEO4JVERSION} ${NEO4JEDITION})|" \ - -e "s|%%NEO4J_EDITION%%|${NEO4JEDITION}|" \ - -e "s|%%NEO4J_DIST_SITE%%|${DISTRIBUTION_SITE}|" \ - "${ADMIN_LOCALCXT_DIR}/Dockerfile" - +sed -i -e "s|%%NEO4J_SHA%%|${coredb_sha}|" "${ADMIN_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_TARBALL%%|$(tarball_name ${NEO4JVERSION} ${NEO4JEDITION})|" "${ADMIN_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_EDITION%%|${NEO4JEDITION}|" "${ADMIN_LOCALCXT_DIR}/Dockerfile" +sed -i -e "s|%%NEO4J_DIST_SITE%%|${DISTRIBUTION_SITE}|" "${ADMIN_LOCALCXT_DIR}/Dockerfile" ## ================================================================================== ## Finally we are ready to do a docker build... @@ -219,5 +212,6 @@ echo -n "${admin_image_tag}" > ${ADMIN_LOCALCXT_DIR}/../.image-id-"${NEO4JEDITIO echo "NEO4JADMIN_IMAGE=$(cat "${ADMIN_LOCALCXT_DIR}"/../.image-id-"${NEO4JEDITION}")" echo "NEO4J_EDITION=${NEO4JEDITION}" echo "NEO4J_SKIP_MOUNTED_FOLDER_TARBALLING=true" -} > ${BUILD_DIR}/devenv-"${NEO4JEDITION}".env +} > ${BUILD_DIR}/${IMAGE_OS}/devenv-"${NEO4JEDITION}".env +ln -f ${BUILD_DIR}/${IMAGE_OS}/devenv-"${NEO4JEDITION}".env ${BUILD_DIR}/devenv-"${NEO4JEDITION}".env diff --git a/docker-image-src/4.4/coredb/docker-entrypoint.sh b/docker-image-src/4.4/coredb/docker-entrypoint.sh index e693fdd8..cd89cb4a 100755 --- a/docker-image-src/4.4/coredb/docker-entrypoint.sh +++ b/docker-image-src/4.4/coredb/docker-entrypoint.sh @@ -126,6 +126,7 @@ function load_plugin_from_location for filename in ${_location}; do echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" cp --preserve "${filename}" "${_destination}" + chmod +rw ${_destination} done if ! is_readable "${_destination}"; then @@ -287,7 +288,6 @@ function set_initial_password admin_user="${BASH_REMATCH[1]}" password="${BASH_REMATCH[2]}" do_reset="${BASH_REMATCH[3]}" - debug_msg "NEO4J_AUTH has been parsed as user \"${admin_user}\", password \"${password}\", do_reset \"${do_reset}\"" if [ "${password}" == "neo4j" ]; then echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." diff --git a/docker-image-src/5/coredb/docker-entrypoint.sh b/docker-image-src/5/coredb/docker-entrypoint.sh index 94141ec1..50d4d0be 100755 --- a/docker-image-src/5/coredb/docker-entrypoint.sh +++ b/docker-image-src/5/coredb/docker-entrypoint.sh @@ -126,6 +126,7 @@ function load_plugin_from_location for filename in ${_location}; do echo "Installing Plugin '${_plugin_name}' from ${_location} to ${_destination}" cp --preserve "${filename}" "${_destination}" + chmod +wr ${_destination} done if ! is_readable "${_destination}"; then @@ -287,6 +288,8 @@ function add_env_setting_to_conf function set_initial_password { + # this has an inbuilt assumption that any configuration settings from the environment have already been applied to neo4j.conf + # This is for the logic to test whether password length is too short. local _neo4j_auth="${1}" # set the neo4j initial password only if you run the database server @@ -298,13 +301,13 @@ function set_initial_password admin_user="${BASH_REMATCH[1]}" password="${BASH_REMATCH[2]}" do_reset="${BASH_REMATCH[3]}" - debug_msg "NEO4J_AUTH has been parsed as user \"${admin_user}\", password \"${password}\", do_reset \"${do_reset}\"" if [ "${password}" == "neo4j" ]; then echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default." exit 1 fi - if [ "${#password}" -lt 8 ]; then + local _min_password_length=$(cat "${NEO4J_HOME}"/conf/neo4j.conf | grep dbms.security.auth_minimum_password_length | sed -E 's/.*=(.*)/\1/') + if [ "${#password}" -lt "${_min_password_length:-"8"}" ]; then echo >&2 "Invalid value for password. The minimum password length is 8 characters. If Neo4j fails to start, you can: 1) Use a stronger password. diff --git a/src/test/java/com/neo4j/docker/coredb/TestAdminReport.java b/src/test/java/com/neo4j/docker/coredb/TestAdminReport.java index dbaa9dfd..57e5b1e8 100644 --- a/src/test/java/com/neo4j/docker/coredb/TestAdminReport.java +++ b/src/test/java/com/neo4j/docker/coredb/TestAdminReport.java @@ -142,6 +142,42 @@ private void verifyCanWriteToMountedLocation(boolean asCurrentUser, String testF } } + @Test + void shouldErrorIfUserCannotWrite() throws Exception + { + try(GenericContainer container = createNeo4jContainer(true)) + { + Path reportFolder = temporaryFolderManager.createTempFolderAndMountAsVolume(container, + outputFolderNamePrefix, + "/reports"); + temporaryFolderManager.setFolderOwnerToNeo4j( reportFolder ); + // now will be running as non root, and try to write to a folder owned by 7474 + container.start(); + Container.ExecResult execResult = container.execInContainer( "neo4j-admin-report", reportDestinationFlag, "/reports" ); + Assertions.assertTrue( execResult.getStderr().contains( "Folder /reports is not accessible for user: " ), + "Did not error about incorrect file permissions" ); + } + } + + @ParameterizedTest(name = "mountPoint_{0}") + @ValueSource(strings = {"/tmp/reports", "/reports"}) + void shouldReownMountedReportDestinationIfRootDoesNotOwn(String mountPoint) throws Exception + { + try(GenericContainer container = createNeo4jContainer(false)) + { + Path reportFolder = temporaryFolderManager.createTempFolderAndMountAsVolume(container, + outputFolderNamePrefix, + mountPoint); + temporaryFolderManager.setFolderOwnerToCurrentUser( reportFolder ); + // now will be running as root, and try to write to a folder owned by 1000 + container.start(); + Container.ExecResult execResult = container.execInContainer( "neo4j-admin-report", reportDestinationFlag, mountPoint ); + Assertions.assertTrue( execResult.getStderr().isEmpty(), + "errors were encountered when trying to reown "+mountPoint+".\n"+execResult.getStderr()); + verifyCreatesReport( reportFolder, execResult ); + } + } + @Test void shouldShowNeo4jAdminHelpText_whenCMD() throws Exception { diff --git a/src/test/java/com/neo4j/docker/coredb/TestMounting.java b/src/test/java/com/neo4j/docker/coredb/TestMounting.java index 56f59b17..2bd2219e 100644 --- a/src/test/java/com/neo4j/docker/coredb/TestMounting.java +++ b/src/test/java/com/neo4j/docker/coredb/TestMounting.java @@ -1,6 +1,5 @@ package com.neo4j.docker.coredb; -import static com.neo4j.docker.utils.StartupDetector.makeContainerWaitForNeo4jReady; import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.model.Bind; import com.neo4j.docker.utils.DatabaseIO; @@ -8,14 +7,6 @@ import com.neo4j.docker.utils.SetContainerUser; import com.neo4j.docker.utils.TemporaryFolderManager; import com.neo4j.docker.utils.TestSettings; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Random; -import java.util.function.Consumer; -import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; @@ -33,90 +24,100 @@ import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.containers.wait.strategy.Wait; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static com.neo4j.docker.utils.StartupDetector.makeContainerWaitForNeo4jReady; + public class TestMounting { - private static Logger log = LoggerFactory.getLogger( TestMounting.class ); + private static Logger log = LoggerFactory.getLogger( TestMounting.class ); @RegisterExtension public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager(); - static Stream defaultUserFlagSecurePermissionsFlag() - { - // "asUser={0}, secureFlag={1}" - // expected behaviour is that if you set --user flag, your data should be read/writable - // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag - // the asCurrentUser=false, secureflag=true combination is tested separately because the container should fail to start. - return Stream.of( - Arguments.arguments( false, false ), - Arguments.arguments( true, false ), - Arguments.arguments( true, true )); - } - - private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet ) - { - log.info( "Running as user {}, {}", - asCurrentUser?"non-root":"root", - isSecurityFlagSet?"with secure file permissions":"with unsecured file permissions" ); - - GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); - container.withExposedPorts( 7474, 7687 ) - .withLogConsumer( new Slf4jLogConsumer( log ) ) - .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) - .withEnv( "NEO4J_AUTH", "none" ); - makeContainerWaitForNeo4jReady( container, "none" ); - if(asCurrentUser) - { - SetContainerUser.nonRootUser( container ); - } - if(isSecurityFlagSet) - { - container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" ); - } - return container; - } - - private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable ) - { - String folderForDiagnostics = folderToCheck.toAbsolutePath().toString(); - - Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" ); - if( shouldBeWritable ) - { - Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host "+folderForDiagnostics+" folder" ); - Assertions.assertTrue(folderToCheck.toFile().canWrite(), "cannot write to host "+folderForDiagnostics+" folder" ); - } - } - - private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable ) - { - verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable ); - - if(TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 )) - { - verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable ); - } - } - - private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable ) - { - verifySingleFolder( logsMount, shouldBeWritable ); - Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(), - "Neo4j did not write a debug.log file to "+logsMount.toString() ); - Assertions.assertEquals( shouldBeWritable, - logsMount.resolve( "debug.log" ).toFile().canWrite(), - String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not ") ); - } - - - @ParameterizedTest(name = "as current user={0}") - @ValueSource(booleans = {true, false}) - void canDumpConfig(boolean asCurrentUser) throws Exception + static Stream defaultUserFlagSecurePermissionsFlag() + { + // "asUser={0}, secureFlag={1}" + // expected behaviour is that if you set --user flag, your data should be read/writable + // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag + // the asCurrentUser=false, secureflag=true combination is tested separately because the container should fail to start. + return Stream.of( + Arguments.arguments( false, false ), + Arguments.arguments( true, false ), + Arguments.arguments( true, true ) ); + } + + private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet ) + { + log.info( "Running as user {}, {}", + asCurrentUser ? "non-root" : "root", + isSecurityFlagSet ? "with secure file permissions" : "with unsecured file permissions" ); + + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_AUTH", "none" ); + makeContainerWaitForNeo4jReady( container, "none" ); + if ( asCurrentUser ) + { + SetContainerUser.nonRootUser( container ); + } + if ( isSecurityFlagSet ) + { + container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" ); + } + return container; + } + + private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable ) + { + String folderForDiagnostics = folderToCheck.toAbsolutePath().toString(); + + Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" ); + if ( shouldBeWritable ) + { + Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host " + folderForDiagnostics + " folder" ); + Assertions.assertTrue( folderToCheck.toFile().canWrite(), "cannot write to host " + folderForDiagnostics + " folder" ); + } + } + + private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable ) + { + verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable ); + + if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 ) ) + { + verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable ); + } + } + + private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable ) + { + verifySingleFolder( logsMount, shouldBeWritable ); + Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(), + "Neo4j did not write a debug.log file to " + logsMount.toString() ); + Assertions.assertEquals( shouldBeWritable, + logsMount.resolve( "debug.log" ).toFile().canWrite(), + String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not " ) ); + } + + @ParameterizedTest( name = "as current user={0}" ) + @ValueSource( booleans = {true, false} ) + void canDumpConfig( boolean asCurrentUser ) throws Exception { File confFile; Path confMount; String assertMsg; String mountPrefix; - if(asCurrentUser) + if ( asCurrentUser ) { assertMsg = "Conf file was not successfully dumped when running container as current user"; mountPrefix = "candumpconf-user-"; @@ -127,10 +128,10 @@ void canDumpConfig(boolean asCurrentUser) throws Exception mountPrefix = "candumpconf-root-"; } - try(GenericContainer container = setupBasicContainer(asCurrentUser, false)) + try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) ) { //Mount /conf - confMount = temporaryFolderManager.createTempFolderAndMountAsVolume(container, mountPrefix,"/conf" ); + confMount = temporaryFolderManager.createTempFolderAndMountAsVolume( container, mountPrefix, "/conf" ); confFile = confMount.resolve( "neo4j.conf" ).toFile(); //Start the container @@ -145,7 +146,7 @@ void canDumpConfig(boolean asCurrentUser) throws Exception // verify conf file was written Assertions.assertTrue( confFile.exists(), assertMsg ); // verify conf folder does not have new owner if not running as root - if(asCurrentUser) + if ( asCurrentUser ) { int fileUID = (Integer) Files.getAttribute( confFile.toPath(), "unix:uid" ); int expectedUID = Integer.parseInt( SetContainerUser.getNonRootUserString().split( ":" )[0] ); @@ -156,7 +157,7 @@ void canDumpConfig(boolean asCurrentUser) throws Exception @Test void canDumpConfig_errorsWithoutConfMount() throws Exception { - try(GenericContainer container = setupBasicContainer( false, false )) + try ( GenericContainer container = setupBasicContainer( false, false ) ) { container.setWaitStrategy( Wait.forLogMessage( ".*Config Dumped.*", 1 ) @@ -164,182 +165,184 @@ void canDumpConfig_errorsWithoutConfMount() throws Exception container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() ); container.setCommand( "dump-config" ); Assertions.assertThrows( ContainerLaunchException.class, - ()->container.start(), - "Did not error when dump config requested without mounted /conf folder"); - String stderr = container.getLogs( OutputFrame.OutputType.STDERR); + () -> container.start(), + "Did not error when dump config requested without mounted /conf folder" ); + String stderr = container.getLogs( OutputFrame.OutputType.STDERR ); Assertions.assertTrue( stderr.endsWith( "You must mount a folder to /conf so that the configuration file(s) can be dumped to there.\n" ) ); } } - @ParameterizedTest(name = "asUser={0}, secureFlag={1}") - @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) - void testCanMountJustDataFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException - { - Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), - "User checks not valid before 3.1" ); - - try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) - { - Path dataMount = temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "canmountjustdata-", - "/data" ); - container.start(); - - // neo4j should now have started, so there'll be stuff in the data folder - // we need to check that stuff is readable and owned by the correct user - verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); - } - } - - @ParameterizedTest(name = "asUser={0}, secureFlag={1}") - @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) - void testCanMountJustLogsFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException - { - Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), - "User checks not valid before 3.1" ); - - try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) - { - Path logsMount = temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "canmountjustlogs-", - "/logs" ); - container.start(); - - verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); - } - } - - @ParameterizedTest(name = "asUser={0}, secureFlag={1}") - @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) - void testCanMountDataAndLogsFolder(boolean asCurrentUser, boolean isSecurityFlagSet) throws IOException - { - Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), - "User checks not valid before 3.1" ); - - try(GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet )) - { - Path testOutputFolder = temporaryFolderManager.createTempFolder( "canmountdataandlogs-" ); - Path dataMount = temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "data-", "/data", testOutputFolder - ); - Path logsMount = temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "logs-", "/logs", testOutputFolder - ); - container.start(); - - verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); - verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); - } - } - - @Test - void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException - { - Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), - "User checks not valid before 3.1" ); - - try(GenericContainer container = setupBasicContainer( false, true )) - { - temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "nopermissioninsecuremode-data-", - "/data" ); - - // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting - container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 ) - .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); - Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, - () -> container.start(), - "Neo4j should not start in secure mode if data folder is unwritable" ); - } - } - - @Test - void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException - { - Assumptions.assumeTrue(TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3,1,0 ) ), - "User checks not valid before 3.1" ); - - try(GenericContainer container = setupBasicContainer( false, true )) - { - temporaryFolderManager.createTempFolderAndMountAsVolume( - container, - "nopermissioninsecuremode-logs-", - "/logs" ); - - // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting - container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 ) - .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); - Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, - () -> container.start(), - "Neo4j should not start in secure mode if logs folder is unwritable" ); - } - } - - @ParameterizedTest(name = "as current user={0}") - @ValueSource(booleans = {true, false}) - void canMountAllTheThings_fileMounts(boolean asCurrentUser) throws Exception - { - Path testOutputFolder = temporaryFolderManager.createTempFolder( "mount-everything-" ); - try(GenericContainer container = setupBasicContainer( asCurrentUser, false )) - { - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "conf", "/conf", testOutputFolder ); - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "data", "/data", testOutputFolder ); - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "import", "/import", testOutputFolder ); - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "logs", "/logs", testOutputFolder ); - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "metrics", "/metrics", testOutputFolder ); - temporaryFolderManager.createTempFolderAndMountAsVolume( container, "plugins", "/plugins", testOutputFolder ); - container.start(); - DatabaseIO databaseIO = new DatabaseIO( container ); - // do some database writes so that we try writing to writable folders. - databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); - databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); - } - } - - @ParameterizedTest(name = "as current user={0}") - @ValueSource(booleans = {true, false}) - void canMountAllTheThings_namedVolumes(boolean asCurrentUser) throws Exception - { - String id = String.format( "%04d", new Random().nextInt( 10000 )); - try(GenericContainer container = setupBasicContainer( asCurrentUser, false )) - { - container.withCreateContainerCmdModifier( - (Consumer) cmd -> cmd.getHostConfig().withBinds( - Bind.parse("conf-"+id+":/conf"), - Bind.parse("data-"+id+":/data"), - Bind.parse("import-"+id+":/import"), - Bind.parse("logs-"+id+":/logs"), - //Bind.parse("metrics-"+id+":/metrics"), //todo metrics needs to be writable but we aren't chowning in the dockerfile, so a named volume for metrics will fail - Bind.parse("plugins-"+id+":/plugins") - )); - container.start(); - DatabaseIO databaseIO = new DatabaseIO( container ); - // do some database writes so that we try writing to writable folders. - databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); - databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); - } - } + @ParameterizedTest( name = "asUser={0}, secureFlag={1}" ) + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountJustDataFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ), + "User checks not valid before 3.1" ); + + try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) ) + { + Path dataMount = temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "canmountjustdata-", + "/data" ); + container.start(); + + // neo4j should now have started, so there'll be stuff in the data folder + // we need to check that stuff is readable and owned by the correct user + verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); + } + } + + @ParameterizedTest( name = "asUser={0}, secureFlag={1}" ) + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountJustLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ), + "User checks not valid before 3.1" ); + + try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) ) + { + Path logsMount = temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "canmountjustlogs-", + "/logs" ); + container.start(); + + verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); + } + } + + @ParameterizedTest( name = "asUser={0}, secureFlag={1}" ) + @MethodSource( "defaultUserFlagSecurePermissionsFlag" ) + void testCanMountDataAndLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ), + "User checks not valid before 3.1" ); + + try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) ) + { + Path testOutputFolder = temporaryFolderManager.createTempFolder( "canmountdataandlogs-" ); + Path dataMount = temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "data-", "/data", testOutputFolder + ); + Path logsMount = temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "logs-", "/logs", testOutputFolder + ); + container.start(); + + verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser ); + verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ), + "User checks not valid before 3.1" ); + + try ( GenericContainer container = setupBasicContainer( false, true ) ) + { + temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-data-", + "/data" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 ) + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if data folder is unwritable" ); + } + } + + @Test + void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ), + "User checks not valid before 3.1" ); + + try ( GenericContainer container = setupBasicContainer( false, true ) ) + { + temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "nopermissioninsecuremode-logs-", + "/logs" ); + + // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting + container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 ) + .withStartupTimeout( Duration.ofSeconds( 20 ) ) ); + Assertions.assertThrows( org.testcontainers.containers.ContainerLaunchException.class, + () -> container.start(), + "Neo4j should not start in secure mode if logs folder is unwritable" ); + } + } + + @ParameterizedTest( name = "as current user={0}" ) + @ValueSource( booleans = {true, false} ) + void canMountAllTheThings_fileMounts( boolean asCurrentUser ) throws Exception + { + Path testOutputFolder = temporaryFolderManager.createTempFolder( "mount-everything-" ); + try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) ) + { + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "conf", "/conf", testOutputFolder ); + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "data", "/data", testOutputFolder ); + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "import", "/import", testOutputFolder ); + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "logs", "/logs", testOutputFolder ); + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "metrics", "/metrics", testOutputFolder ); + temporaryFolderManager.createTempFolderAndMountAsVolume( container, "plugins", "/plugins", testOutputFolder ); + container.start(); + DatabaseIO databaseIO = new DatabaseIO( container ); + // do some database writes so that we try writing to writable folders. + databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); + databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); + } + } + + @ParameterizedTest( name = "as current user={0}" ) + @ValueSource( booleans = {true, false} ) + void canMountAllTheThings_namedVolumes( boolean asCurrentUser ) throws Exception + { + String id = String.format( "%04d", new Random().nextInt( 10000 ) ); + try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) ) + { + container.withCreateContainerCmdModifier( + (Consumer) cmd -> cmd.getHostConfig().withBinds( + Bind.parse( "conf-" + id + ":/conf" ), + Bind.parse( "data-" + id + ":/data" ), + Bind.parse( "import-" + id + ":/import" ), + Bind.parse( "logs-" + id + ":/logs" ), + //Bind.parse("metrics-"+id+":/metrics"), //todo metrics needs to be writable but we aren't chowning in the dockerfile, so a named volume for metrics will fail + Bind.parse( "plugins-" + id + ":/plugins" ) + ) ); + container.start(); + DatabaseIO databaseIO = new DatabaseIO( container ); + // do some database writes so that we try writing to writable folders. + databaseIO.putInitialDataIntoContainer( "neo4j", "none" ); + databaseIO.verifyInitialDataInContainer( "neo4j", "none" ); + } + } @Test - void shouldReownSubfilesToNeo4j() throws Exception { + void shouldReownSubfilesToNeo4j() throws Exception + { Assumptions.assumeTrue( - TestSettings.NEO4J_VERSION.isAtLeastVersion(new Neo4jVersion(4, 0, 0)), - "User checks not valid before 4.0"); + TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 0, 0 ) ), + "User checks not valid before 4.0" ); Path logMount = temporaryFolderManager.createTempFolder( "subfileownership-" ); - Path debugLog = logMount.resolve("debug.log"); + Path debugLog = logMount.resolve( "debug.log" ); // put file in logMount - Files.write(debugLog, "some log words".getBytes()); + Files.write( debugLog, "some log words".getBytes() ); // make neo4j own the conf folder but NOT the neo4j.conf temporaryFolderManager.setFolderOwnerToNeo4j( logMount ); temporaryFolderManager.setFolderOwnerToCurrentUser( debugLog ); - try (GenericContainer container = setupBasicContainer(false, false)) { + try ( GenericContainer container = setupBasicContainer( false, false ) ) + { temporaryFolderManager.mountHostFolderAsVolume( container, logMount, "/logs" ); container.start(); // if debug.log doesn't get re-owned, neo4j will not start and this test will fail here diff --git a/src/test/java/com/neo4j/docker/coredb/TestPasswords.java b/src/test/java/com/neo4j/docker/coredb/TestPasswords.java index 3ceda929..6e659ef3 100644 --- a/src/test/java/com/neo4j/docker/coredb/TestPasswords.java +++ b/src/test/java/com/neo4j/docker/coredb/TestPasswords.java @@ -1,5 +1,7 @@ package com.neo4j.docker.coredb; +import com.neo4j.docker.coredb.configurations.Configuration; +import com.neo4j.docker.coredb.configurations.Setting; import com.neo4j.docker.utils.DatabaseIO; import com.neo4j.docker.utils.Neo4jVersion; import com.neo4j.docker.utils.SetContainerUser; @@ -21,6 +23,8 @@ import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.containers.wait.strategy.Wait; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -79,30 +83,11 @@ void testPasswordCantBeNeo4j() throws Exception failContainer.followOutput( waitingConsumer ); Assertions.assertDoesNotThrow( () -> waitingConsumer.waitUntil( - frame -> frame.getUtf8String().contains("Invalid value for password" ), 10, TimeUnit.SECONDS ), + frame -> frame.getUtf8String().contains("Invalid value for password" ), 20, TimeUnit.SECONDS ), "did not error due to invalid password" ); } } - @Test - void testWarnAndFailIfPasswordLessThan8Chars() throws Exception - { - Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ), - "Minimum password length introduced in 5.2.0"); - try(GenericContainer failContainer = createContainer( false )) - { - failContainer.withEnv( "NEO4J_AUTH", "neo4j/123" ) - .withStartupCheckStrategy( new OneShotStartupCheckStrategy() ); - Assertions.assertThrows( ContainerLaunchException.class, () -> failContainer.start(), - "Neo4j started even though initial password was too short" ); - String logsOut = failContainer.getLogs(); - Assertions.assertTrue( logsOut.contains( "Invalid value for password" ), - "did not error due to too short password"); - Assertions.assertFalse( logsOut.contains( "Remote interface available at http://localhost:7474/" ), - "Neo4j started even though an invalid password was set"); - } - } - @Test void testDefaultPasswordAndPasswordResetIfNoNeo4jAuthSet() { @@ -240,4 +225,85 @@ void testPromptsForPasswordReset() db.verifyInitialDataInContainer( user, resetPass ); } } + + @Test + void testWarnAndFailIfPasswordLessThan8Chars() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ), + "Minimum password length introduced in 5.2.0"); + try(GenericContainer failContainer = createContainer( false )) + { + failContainer.withEnv( "NEO4J_AUTH", "neo4j/123" ) + .withStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + Assertions.assertThrows( ContainerLaunchException.class, () -> failContainer.start(), + "Neo4j started even though initial password was too short" ); + String logsOut = failContainer.getLogs(); + Assertions.assertTrue( logsOut.contains( "Invalid value for password" ), + "did not error due to too short password"); + Assertions.assertFalse( logsOut.contains( "Remote interface available at http://localhost:7474/" ), + "Neo4j started even though an invalid password was set"); + } + } + + @Test + void testWarnAndFailIfPasswordLessThanOverride() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ), + "Minimum password length introduced in 5.2.0"); + try(GenericContainer failContainer = createContainer( false )) + { + failContainer.withEnv( "NEO4J_AUTH", "neo4j/123" ) + .withEnv(Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).envName, "20") + .withStartupCheckStrategy( new OneShotStartupCheckStrategy() ); + Assertions.assertThrows( ContainerLaunchException.class, () -> failContainer.start(), + "Neo4j started even though initial password was too short" ); + String logsOut = failContainer.getLogs(); + Assertions.assertTrue( logsOut.contains( "Invalid value for password" ), + "did not error due to too short password"); + Assertions.assertFalse( logsOut.contains( "Remote interface available at http://localhost:7474/" ), + "Neo4j started even though an invalid password was set"); + } + } + + @Test + void shouldNotWarnAboutMinimumPasswordLengthIfSettingOverridden_env() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ), + "Minimum password length introduced in 5.2.0"); + try(GenericContainer container = createContainer( false )) + { + container.withEnv( "NEO4J_AUTH", "neo4j/123" ) + .withEnv(Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).envName, "2"); + container.start(); + String logs = container.getLogs(); + Assertions.assertFalse( logs.contains( "Invalid value for password. The minimum password length is 8 characters." ), + "Should not error about minimum password length if overridden."); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( "neo4j", "123" ); + } + } + + @Test + void shouldNotWarnAboutMinimumPasswordLengthIfSettingOverridden_conf() throws Exception + { + Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5,2,0 ) ), + "Minimum password length introduced in 5.2.0"); + try(GenericContainer container = createContainer( false )) + { + Path confMount = temporaryFolderManager.createTempFolderAndMountAsVolume( + container, + "noMinimumPasswordLength-conf-", + "/conf" ); + Files.writeString(confMount.resolve( "neo4j.conf" ), + Configuration.getConfigurationNameMap().get( Setting.MINIMUM_PASSWORD_LENGTH ).name+"=2"); + + container.withEnv( "NEO4J_AUTH", "neo4j/123" ); + container.start(); + String logs = container.getLogs(); + Assertions.assertFalse( logs.contains( "Invalid value for password. The minimum password length is 8 characters." ), + "Should not error about minimum password length if overridden."); + DatabaseIO db = new DatabaseIO( container ); + db.putInitialDataIntoContainer( "neo4j", "123" ); + } + } } diff --git a/src/test/java/com/neo4j/docker/coredb/configurations/Configuration.java b/src/test/java/com/neo4j/docker/coredb/configurations/Configuration.java index c3f286c0..8e3b430c 100644 --- a/src/test/java/com/neo4j/docker/coredb/configurations/Configuration.java +++ b/src/test/java/com/neo4j/docker/coredb/configurations/Configuration.java @@ -26,6 +26,7 @@ public class Configuration put( Setting.MEMORY_HEAP_INITIALSIZE, new Configuration("server.memory.heap.initial_size")); put( Setting.MEMORY_HEAP_MAXSIZE, new Configuration( "server.memory.heap.max_size")); put( Setting.MEMORY_PAGECACHE_SIZE, new Configuration("server.memory.pagecache.size")); + put( Setting.MINIMUM_PASSWORD_LENGTH, new Configuration("dbms.security.auth_minimum_password_length")); put( Setting.SECURITY_PROCEDURES_UNRESTRICTED, new Configuration("dbms.security.procedures.unrestricted")); put( Setting.TXLOG_RETENTION_POLICY, new Configuration("db.tx_log.rotation.retention_policy")); }}; diff --git a/src/test/java/com/neo4j/docker/coredb/configurations/Setting.java b/src/test/java/com/neo4j/docker/coredb/configurations/Setting.java index 32f1a389..b2d9a4a2 100644 --- a/src/test/java/com/neo4j/docker/coredb/configurations/Setting.java +++ b/src/test/java/com/neo4j/docker/coredb/configurations/Setting.java @@ -17,6 +17,7 @@ public enum Setting MEMORY_HEAP_INITIALSIZE, MEMORY_HEAP_MAXSIZE, MEMORY_PAGECACHE_SIZE, + MINIMUM_PASSWORD_LENGTH, SECURITY_PROCEDURES_UNRESTRICTED, TXLOG_RETENTION_POLICY } diff --git a/src/test/java/com/neo4j/docker/coredb/configurations/TestExtendedConf.java b/src/test/java/com/neo4j/docker/coredb/configurations/TestExtendedConf.java index 6a06324a..616ac490 100644 --- a/src/test/java/com/neo4j/docker/coredb/configurations/TestExtendedConf.java +++ b/src/test/java/com/neo4j/docker/coredb/configurations/TestExtendedConf.java @@ -91,7 +91,7 @@ private void assertPasswordChangedLogIsCorrect( String password, GenericContaine } } - @ParameterizedTest + @ParameterizedTest(name = "default_user_password_{0}") @ValueSource(strings = {"", "supersecretpassword"}) void testReadsTheExtendedConfFile_defaultUser(String password) throws Exception { @@ -104,11 +104,13 @@ void testReadsTheExtendedConfFile_defaultUser(String password) throws Exception Path confFile = testConfsFolder.resolve( "ExtendedConf.conf" ); Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) ); chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) ); - temporaryFolderManager.setFolderOwnerToNeo4j( confFolder.resolve( "neo4j.conf" ) ); + //temporaryFolderManager.setFolderOwnerToNeo4j( confFolder.resolve( "neo4j.conf" ) ); // start container try(GenericContainer container = createContainer(password)) { + container.withEnv( "NEO4J_DEBUG", "yes" ) + .withEnv( "NEO4J_server_cluster_listen__address", "$(hostname):6000" ); runContainerAndVerify( container, confFolder, logsFolder, password ); } } diff --git a/src/test/java/com/neo4j/docker/coredb/plugins/TestBundledPluginInstallation.java b/src/test/java/com/neo4j/docker/coredb/plugins/TestBundledPluginInstallation.java index ce3c588a..669ea6b3 100644 --- a/src/test/java/com/neo4j/docker/coredb/plugins/TestBundledPluginInstallation.java +++ b/src/test/java/com/neo4j/docker/coredb/plugins/TestBundledPluginInstallation.java @@ -61,7 +61,7 @@ private GenericContainer createContainerWithBundledPlugin(String pluginName) .waitingFor( Wait.forHttp( "/" ) .forPort( DEFAULT_BROWSER_PORT ) .forStatusCode( 200 ) - .withStartupTimeout( Duration.ofSeconds( 45 ) ) ); + .withStartupTimeout( Duration.ofSeconds( 60 ) ) ); return container; } @@ -205,7 +205,8 @@ void testPluginLoadsWithAuthentication() throws Exception .withEnv( "NEO4J_dbms_bloom_license__file", "/licenses/bloom.license" ); // mounting logs because it's useful for debugging temporaryFolderManager.createTempFolderAndMountAsVolume( container, "logs", "/logs", testFolder ); - + Path licenseFolder = temporaryFolderManager.createTempFolderAndMountAsVolume( container, "license", "/licenses", testFolder ); + Files.writeString( licenseFolder.resolve("bloom.license"), "notareallicense" ); // make sure the container successfully starts and we can write to it without getting authentication errors container.start(); DatabaseIO dbio = new DatabaseIO( container ); diff --git a/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java b/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java index e3812ea0..95620254 100644 --- a/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java +++ b/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java @@ -2,14 +2,22 @@ import com.github.dockerjava.api.command.CreateContainerCmd; import com.google.gson.Gson; -import com.neo4j.docker.utils.*; -import java.time.Duration; +import com.neo4j.docker.utils.DatabaseIO; +import com.neo4j.docker.utils.HostFileHttpHandler; +import com.neo4j.docker.utils.HttpServerRule; +import com.neo4j.docker.utils.Neo4jVersion; +import com.neo4j.docker.utils.SetContainerUser; +import com.neo4j.docker.utils.StartupDetector; +import com.neo4j.docker.utils.TemporaryFolderManager; +import com.neo4j.docker.utils.TestSettings; import org.junit.Rule; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.Testcontainers; @@ -24,6 +32,7 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -34,6 +43,7 @@ import org.neo4j.driver.Record; +import static com.neo4j.docker.utils.StartupDetector.makeContainerWaitForNeo4jReady; import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION; @EnableRuleMigrationSupport @@ -55,21 +65,40 @@ private GenericContainer createContainerWithTestingPlugin() Testcontainers.exposeHostPorts( httpServer.PORT ); GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); - container.withEnv( "NEO4J_AUTH", DB_USER+"/"+ DB_PASSWORD) - .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) - .withEnv( "NEO4J_DEBUG", "yes" ) - .withEnv( Neo4jPluginEnv.get(), "[\"_testing\"]" ) - .withExposedPorts( 7474, 7687 ) - .withLogConsumer( new Slf4jLogConsumer( log ) ); - StartupDetector.makeContainerWaitForDatabaseReady(container, DB_USER, DB_PASSWORD, "neo4j", - Duration.ofSeconds(60)); + container.withEnv( "NEO4J_AUTH", DB_USER + "/" + DB_PASSWORD ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_DEBUG", "yes" ) + .withEnv( Neo4jPluginEnv.get(), "[\"_testing\"]" ) + .withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ); + StartupDetector.makeContainerWaitForDatabaseReady( container, DB_USER, DB_PASSWORD, "neo4j", + Duration.ofSeconds( 60 ) ); SetContainerUser.nonRootUser( container ); return container; } - private File createTestVersionsJson(Path destinationFolder, String version) throws Exception + private GenericContainer setupContainerWithUser( boolean asCurrentUser ) { - List jsonEntry = Collections.singletonList( new VersionsJsonEntry(version) ); + log.info( "Running as user {}, {}", + asCurrentUser ? "non-root" : "root" ); + + GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ); + container.withExposedPorts( 7474, 7687 ) + .withLogConsumer( new Slf4jLogConsumer( log ) ) + .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) + .withEnv( "NEO4J_AUTH", "none" ); + makeContainerWaitForNeo4jReady( container, "none" ); + if ( asCurrentUser ) + { + SetContainerUser.nonRootUser( container ); + } + + return container; + } + + private File createTestVersionsJson( Path destinationFolder, String version ) throws Exception + { + List jsonEntry = Collections.singletonList( new VersionsJsonEntry( version ) ); Gson jsonBuilder = new Gson(); String jsonStr = jsonBuilder.toJson( jsonEntry ); @@ -78,12 +107,12 @@ private File createTestVersionsJson(Path destinationFolder, String version) thro return outputJsonFile; } - private File createTestVersionsJson(Path destinationFolder, Map versionAndJar) throws Exception + private File createTestVersionsJson( Path destinationFolder, Map versionAndJar ) throws Exception { List jsonEntries = versionAndJar.keySet() .stream() .map( key -> new VersionsJsonEntry( key, versionAndJar.get( key ) ) ) - .collect( Collectors.toList()); + .collect( Collectors.toList() ); Gson jsonBuilder = new Gson(); String jsonStr = jsonBuilder.toJson( jsonEntries ); @@ -94,13 +123,13 @@ private File createTestVersionsJson(Path destinationFolder, Map private void setupTestPlugin( File versionsJson ) throws Exception { - File myPluginJar = new File(getClass().getClassLoader().getResource( "testplugin/"+PLUGIN_JAR ).toURI()); + File myPluginJar = new File( getClass().getClassLoader().getResource( "testplugin/" + PLUGIN_JAR ).toURI() ); httpServer.registerHandler( versionsJson.getName(), new HostFileHttpHandler( versionsJson, "application/json" ) ); httpServer.registerHandler( PLUGIN_JAR, new HostFileHttpHandler( myPluginJar, "application/java-archive" ) ); } - private void verifyTestPluginLoaded(DatabaseIO db) + private void verifyTestPluginLoaded( DatabaseIO db ) { // when we check the list of installed procedures... String listProceduresCypherQuery = NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 3, 0 ) ) ? @@ -109,17 +138,17 @@ private void verifyTestPluginLoaded(DatabaseIO db) List procedures = db.runCypherQuery( DB_USER, DB_PASSWORD, listProceduresCypherQuery ); // Then the procedure from the test plugin should be listed Assertions.assertTrue( procedures.stream() - .anyMatch(x -> x.get( "name" ).asString() - .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ), - "Missing procedure provided by our plugin" ); + .anyMatch( x -> x.get( "name" ).asString() + .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ), + "Missing procedure provided by our plugin" ); // When we call the procedure from the plugin - List pluginResponse = db.runCypherQuery(DB_USER, DB_PASSWORD, - "CALL com.neo4j.docker.test.myplugin.defaultValues" ); + List pluginResponse = db.runCypherQuery( DB_USER, DB_PASSWORD, + "CALL com.neo4j.docker.test.myplugin.defaultValues" ); // Then we get the response we expect - Assertions.assertEquals(1, pluginResponse.size(), "Our procedure should only return a single result"); - Record record = pluginResponse.get(0); + Assertions.assertEquals( 1, pluginResponse.size(), "Our procedure should only return a single result" ); + Record record = pluginResponse.get( 0 ); String message = "Result from calling our procedure doesnt match our expectations"; Assertions.assertEquals( "a string", record.get( "string" ).asString(), message ); @@ -134,11 +163,11 @@ public void testPluginLoads() throws Exception Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-" ); File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } @@ -146,39 +175,39 @@ public void testPluginLoads() throws Exception public void test_NEO4JLABS_PLUGIN_envWorksIn5() throws Exception { Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ), - "NEO4JLABS_PLUGIN backwards compatibility does not need checking pre 5.x"); + "NEO4JLABS_PLUGIN backwards compatibility does not need checking pre 5.x" ); Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-backcompat-" ); File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "" ); container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "[\"_testing\"]" ); container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } @Test public void test_NEO4J_PLUGIN_envWorksIn44() throws Exception { - Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4,4,18 ) ), - "NEO4JLABS_PLUGIN did not work in 4.4 before 4.4.18"); + Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 4, 18 ) ), + "NEO4JLABS_PLUGIN did not work in 4.4 before 4.4.18" ); Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ), "Only checking forwards compatibility in 4.4" ); Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-forwardcompat-" ); File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "[\"_testing\"]" ); container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "" ); container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } @@ -188,21 +217,21 @@ public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Excepti Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-noOverride-" ); File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.toString() ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { // When we set a config value explicitly - container.withEnv("NEO4J_dbms_security_procedures_unrestricted", "foo" ); + container.withEnv( "NEO4J_dbms_security_procedures_unrestricted", "foo" ); // When we start the neo4j docker container container.start(); // When we connect to the database with the plugin // Check that the config remains as set by our env var and is not overridden by the plugin defaults - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); db.verifyConfigurationSetting( DB_USER, DB_PASSWORD, "dbms.security.procedures.unrestricted", "foo", - "neo4j config should not be overridden by plugin"); + "neo4j config should not be overridden by plugin" ); } } @@ -210,18 +239,18 @@ public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Excepti void invalidPluginNameShouldGiveOptionsAndError() { Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) ); - try(GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )) + try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) ) { // if we try to set a plugin that doesn't exist container.withEnv( Neo4jPluginEnv.get(), "[\"notarealplugin\"]" ) .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) .withStartupCheckStrategy( new OneShotStartupCheckStrategy() - .withTimeout( Duration.ofSeconds( 10 ) ) ) + .withTimeout( Duration.ofSeconds( 30 ) ) ) .withLogConsumer( new Slf4jLogConsumer( log ) ); Assertions.assertThrows( ContainerLaunchException.class, container::start ); // the container should output a helpful message and quit String stdout = container.getLogs(); - Assertions.assertTrue( stdout.contains("\"notarealplugin\" is not a known Neo4j plugin. Options are:") ); + Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) ); Assertions.assertFalse( stdout.contains( "_testing" ), "Fake _testing plugin is exposed." ); } } @@ -230,18 +259,18 @@ void invalidPluginNameShouldGiveOptionsAndError() void invalidPluginNameShouldGiveOptionsAndError_mulitpleplugins() { Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) ); - try(GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )) + try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) ) { // if we try to set a plugin that doesn't exist container.withEnv( Neo4jPluginEnv.get(), "[\"apoc\", \"notarealplugin\"]" ) .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" ) .withStartupCheckStrategy( new OneShotStartupCheckStrategy() - .withTimeout( Duration.ofSeconds( 10 ) ) ) + .withTimeout( Duration.ofSeconds( 30 ) ) ) .withLogConsumer( new Slf4jLogConsumer( log ) ); Assertions.assertThrows( ContainerLaunchException.class, container::start ); // the container should output a helpful message and quit String stdout = container.getLogs(); - Assertions.assertTrue( stdout.contains("\"notarealplugin\" is not a known Neo4j plugin. Options are:") ); + Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) ); Assertions.assertFalse( stdout.contains( "_testing" ), "Fake _testing plugin is exposed." ); } } @@ -252,22 +281,22 @@ public void testBrokenVersionsJsonCausesHelpfulError() throws Exception Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) ); Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-broken-versionsjson-" ); // create a versions.json that DOES NOT contain the current neo4j version in its mapping - File versionsJson = createTestVersionsJson( pluginsDir, "50.0.0"); + File versionsJson = createTestVersionsJson( pluginsDir, "50.0.0" ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.start(); - String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR); - Assertions.assertTrue( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j "+NEO4J_VERSION ), - "Did not error about plugin compatibility."); - DatabaseIO db = new DatabaseIO(container); + String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR ); + Assertions.assertTrue( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j " + NEO4J_VERSION ), + "Did not error about plugin compatibility." ); + DatabaseIO db = new DatabaseIO( container ); // make sure plugin did not load List procedures = db.runCypherQuery( DB_USER, DB_PASSWORD, "SHOW PROCEDURES YIELD name, signature RETURN name, signature" ); Assertions.assertFalse( procedures.stream() - .anyMatch(x -> x.get( "name" ).asString() - .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ), - "Incompatible test plugin was loaded." ); + .anyMatch( x -> x.get( "name" ).asString() + .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ), + "Incompatible test plugin was loaded." ); } } @@ -275,13 +304,13 @@ public void testBrokenVersionsJsonCausesHelpfulError() throws Exception void testSemanticVersioningPlugin_catchesMatchWithX() throws Exception { Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-semverMatchesX-" ); - File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".x"); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch() + ".x" ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } @@ -289,13 +318,13 @@ void testSemanticVersioningPlugin_catchesMatchWithX() throws Exception void testSemanticVersioningPlugin_catchesMatchWithStar() throws Exception { Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-semverMatchesStar-" ); - File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".*"); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch() + ".*" ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } @@ -307,15 +336,15 @@ void testSemanticVersioningPlugin_prefersExactMatch() throws Exception {{ put( NEO4J_VERSION.toString(), PLUGIN_JAR ); put( NEO4J_VERSION.getBranch() + ".x", "notareal.jar" ); - put( NEO4J_VERSION.major+ ".x.x", "notareal.jar" ); - }}); + put( NEO4J_VERSION.major + ".x.x", "notareal.jar" ); + }} ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.start(); - DatabaseIO db = new DatabaseIO(container); + DatabaseIO db = new DatabaseIO( container ); // if semver did not pick exact version match then it will load a non-existent plugin instead and fail. - verifyTestPluginLoaded(db); + verifyTestPluginLoaded( db ); } } @@ -323,58 +352,59 @@ void testSemanticVersioningPlugin_prefersExactMatch() throws Exception public void testPlugin_originalEntrypointLocation() throws Exception { Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ), - "/docker-entrypoint.sh is permanently moved from 5.0 onwards"); + "/docker-entrypoint.sh is permanently moved from 5.0 onwards" ); Path pluginsDir = temporaryFolderManager.createTempFolder( "plugin-oldEntrypoint-" ); - File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch()+".x" ); + File versionsJson = createTestVersionsJson( pluginsDir, NEO4J_VERSION.getBranch() + ".x" ); setupTestPlugin( versionsJson ); - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.withCreateContainerCmdModifier( (Consumer) cmd -> cmd.withEntrypoint( "/docker-entrypoint.sh", "neo4j" ) ); container.start(); - DatabaseIO db = new DatabaseIO(container); - verifyTestPluginLoaded(db); + DatabaseIO db = new DatabaseIO( container ); + verifyTestPluginLoaded( db ); } } - @Test void testSemanticVersioningLogic() throws Exception { - String major = Integer.toString(NEO4J_VERSION.major); - String minor = Integer.toString(NEO4J_VERSION.minor); + String major = Integer.toString( NEO4J_VERSION.major ); + String minor = Integer.toString( NEO4J_VERSION.minor ); // testing common neo4j name variants - List neo4jVersions = new ArrayList() {{ - add(NEO4J_VERSION.toString()); - add(NEO4J_VERSION.toString()+"-drop01.1"); - add(NEO4J_VERSION.toString()+"-drop01"); - add(NEO4J_VERSION.toString()+"-beta04"); + List neo4jVersions = new ArrayList() + {{ + add( NEO4J_VERSION.toString() ); + add( NEO4J_VERSION.toString() + "-drop01.1" ); + add( NEO4J_VERSION.toString() + "-drop01" ); + add( NEO4J_VERSION.toString() + "-beta04" ); }}; - List matchingCases = new ArrayList() {{ + List matchingCases = new ArrayList() + {{ add( NEO4J_VERSION.toString() ); - add( major+'.'+minor+".x" ); - add( major+'.'+minor+".*" ); + add( major + '.' + minor + ".x" ); + add( major + '.' + minor + ".*" ); }}; - List nonMatchingCases = new ArrayList() {{ - add( (NEO4J_VERSION.major+1)+'.'+minor+".x" ); - add( (NEO4J_VERSION.major-1)+'.'+minor+".x" ); - add( major+'.'+(NEO4J_VERSION.minor+1)+".x" ); - add( major+'.'+(NEO4J_VERSION.minor-1)+".x" ); - add( (NEO4J_VERSION.major+1)+'.'+minor+".*" ); - add( (NEO4J_VERSION.major-1)+'.'+minor+".*" ); - add( major+'.'+(NEO4J_VERSION.minor+1)+".*" ); - add( major+'.'+(NEO4J_VERSION.minor-1)+".*" ); + List nonMatchingCases = new ArrayList() + {{ + add( (NEO4J_VERSION.major + 1) + '.' + minor + ".x" ); + add( (NEO4J_VERSION.major - 1) + '.' + minor + ".x" ); + add( major + '.' + (NEO4J_VERSION.minor + 1) + ".x" ); + add( major + '.' + (NEO4J_VERSION.minor - 1) + ".x" ); + add( (NEO4J_VERSION.major + 1) + '.' + minor + ".*" ); + add( (NEO4J_VERSION.major - 1) + '.' + minor + ".*" ); + add( major + '.' + (NEO4J_VERSION.minor + 1) + ".*" ); + add( major + '.' + (NEO4J_VERSION.minor - 1) + ".*" ); }}; // Asserting every test case means that if there's a failure, all further tests won't run. // Instead we're running all tests and saving any failed cases for reporting at the end of the test. List failedTests = new ArrayList(); - - try(GenericContainer container = createContainerWithTestingPlugin()) + try ( GenericContainer container = createContainerWithTestingPlugin() ) { container.withEnv( Neo4jPluginEnv.get(), "" ); // don't need the _testing plugin for this container.start(); @@ -382,31 +412,30 @@ void testSemanticVersioningLogic() throws Exception String semverQuery = "echo \"{\\\"neo4j\\\":\\\"%s\\\"}\" | " + "jq -L/startup --raw-output \"import \\\"semver\\\" as lib; " + ".neo4j | lib::semver(\\\"%s\\\")\""; - for(String neoVer : neo4jVersions) + for ( String neoVer : neo4jVersions ) { - for(String ver : matchingCases) + for ( String ver : matchingCases ) { - Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer) ); - if(! out.getStdout().trim().equals( "true" ) ) + Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer ) ); + if ( !out.getStdout().trim().equals( "true" ) ) { - failedTests.add( String.format( "%s should match %s but did not", ver, neoVer) ); + failedTests.add( String.format( "%s should match %s but did not", ver, neoVer ) ); } } - for(String ver : nonMatchingCases) + for ( String ver : nonMatchingCases ) { - Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer) ); - if(! out.getStdout().trim().equals( "false" ) ) + Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, ver, neoVer ) ); + if ( !out.getStdout().trim().equals( "false" ) ) { - failedTests.add( String.format( "%s should NOT match %s but did", ver, neoVer) ); + failedTests.add( String.format( "%s should NOT match %s but did", ver, neoVer ) ); } } } - if(failedTests.size() > 0) + if ( failedTests.size() > 0 ) { - Assertions.fail(failedTests.stream().collect( Collectors.joining("\n"))); + Assertions.fail( failedTests.stream().collect( Collectors.joining( "\n" ) ) ); } } - } private class VersionsJsonEntry @@ -415,18 +444,42 @@ private class VersionsJsonEntry String jar; String _testing; - VersionsJsonEntry(String neo4j) + VersionsJsonEntry( String neo4j ) { this.neo4j = neo4j; this._testing = "SNAPSHOT"; - this.jar = "http://host.testcontainers.internal:3000/"+PLUGIN_JAR; + this.jar = "http://host.testcontainers.internal:3000/" + PLUGIN_JAR; } - VersionsJsonEntry(String neo4j, String jar) + VersionsJsonEntry( String neo4j, String jar ) { this.neo4j = neo4j; this._testing = "SNAPSHOT"; - this.jar = "http://host.testcontainers.internal:3000/"+jar; + this.jar = "http://host.testcontainers.internal:3000/" + jar; + } + } + + @ParameterizedTest( name = "as current user={0}" ) + @ValueSource( booleans = {true, false} ) + void testPluginIsMovedToMountedFolderAndIsLoadedCorrectly( boolean asCurrentUser ) throws Exception + { + Path testOutputFolder = temporaryFolderManager.createTempFolder( "mount-plugins-only-" ); + try ( GenericContainer container = setupContainerWithUser( asCurrentUser ) ) + { + var pluginsFolder = temporaryFolderManager.createTempFolderAndMountAsVolume( container, "plugins", "/plugins", testOutputFolder ); + container.withEnv( "NEO4J_PLUGINS", "[\"bloom\"]" ); + container.start(); + + Assertions.assertTrue( pluginsFolder.resolve( "bloom.jar" ).toFile().exists(), "Did not find bloom.jar in plugins folder" ); + assertBloomIsLoaded( container ); } } + + void assertBloomIsLoaded( GenericContainer container ) + { + DatabaseIO databaseIO = new DatabaseIO( container ); + + var result = databaseIO.runCypherQuery( "neo4j", "none", "SHOW PROCEDURES YIELD name, description, signature WHERE name STARTS WITH 'bloom'" ); + Assertions.assertFalse( result.isEmpty(), "Bloom procedures not found in neo4j installation" ); + } } diff --git a/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java b/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java index ba520897..43e9b49b 100644 --- a/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java +++ b/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java @@ -64,7 +64,7 @@ public void beforeEach( ExtensionContext extensionContext ) throws Exception extensionContext.getTestMethod().get().getName(); if(!extensionContext.getDisplayName().startsWith( extensionContext.getTestMethod().get().getName() )) { - outputFolderNamePrefix += "_" + extensionContext.getDisplayName() + "-"; + outputFolderNamePrefix += "_" + extensionContext.getDisplayName().replace( "/", "\\/" ) + "-"; } else {