#
tokens: 47388/50000 14/140 files (page 3/7)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 7. Use http://codebase.md/chillbruhhh/crawl4ai-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .dockerignore
├── .env.example
├── .gitattributes
├── .gitignore
├── crawled_pages.sql
├── Dockerfile
├── knowledge_graphs
│   ├── ai_hallucination_detector.py
│   ├── ai_script_analyzer.py
│   ├── hallucination_reporter.py
│   ├── knowledge_graph_validator.py
│   ├── parse_repo_into_neo4j.py
│   ├── query_knowledge_graph.py
│   └── test_script.py
├── LICENSE
├── neo4j
│   └── docker-neo4j
│       ├── .github
│       │   └── ISSUE_TEMPLATE
│       │       └── bug_report.md
│       ├── .gitignore
│       ├── build-docker-image.sh
│       ├── build-utils-common-functions.sh
│       ├── COPYRIGHT
│       ├── DEVELOPMENT.md
│       ├── devenv
│       ├── devenv.local.template
│       ├── docker-image-src
│       │   ├── 2.3
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.0
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.1
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.2
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.3
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.4
│       │   │   ├── docker-entrypoint.sh
│       │   │   └── Dockerfile
│       │   ├── 3.5
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.0
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   └── Dockerfile
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.1
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   └── Dockerfile
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.2
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.3
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       └── Dockerfile
│       │   ├── 4.4
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       └── Dockerfile-ubi9
│       │   ├── 5
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi8
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       ├── Dockerfile-ubi8
│       │   │       └── Dockerfile-ubi9
│       │   ├── calver
│       │   │   ├── coredb
│       │   │   │   ├── docker-entrypoint.sh
│       │   │   │   ├── Dockerfile-debian
│       │   │   │   ├── Dockerfile-ubi9
│       │   │   │   ├── neo4j-admin-report.sh
│       │   │   │   └── neo4j-plugins.json
│       │   │   └── neo4j-admin
│       │   │       ├── docker-entrypoint.sh
│       │   │       ├── Dockerfile-debian
│       │   │       └── Dockerfile-ubi9
│       │   └── common
│       │       ├── semver.jq
│       │       └── utilities.sh
│       ├── generate-stub-plugin
│       │   ├── build.gradle.kts
│       │   ├── Dockerfile
│       │   ├── ExampleNeo4jPlugin.java
│       │   ├── Makefile
│       │   ├── README.md
│       │   └── settings.gradle.kts
│       ├── LICENSE
│       ├── Makefile
│       ├── pom.xml
│       ├── publish-neo4j-admin-image.sh
│       ├── publish-neo4j-admin-images.sh
│       ├── README.md
│       └── src
│           ├── main
│           │   └── resources
│           │       └── log4j.properties
│           └── test
│               ├── java
│               │   └── com
│               │       └── neo4j
│               │           └── docker
│               │               ├── coredb
│               │               │   ├── configurations
│               │               │   │   ├── Configuration.java
│               │               │   │   ├── Setting.java
│               │               │   │   ├── TestConfSettings.java
│               │               │   │   ├── TestExtendedConf.java
│               │               │   │   └── TestJVMAdditionalConfig.java
│               │               │   ├── plugins
│               │               │   │   ├── Neo4jPluginEnv.java
│               │               │   │   ├── StubPluginHelper.java
│               │               │   │   ├── TestBundledPluginInstallation.java
│               │               │   │   ├── TestPluginInstallation.java
│               │               │   │   └── TestSemVerPluginMatching.java
│               │               │   ├── TestAdminReport.java
│               │               │   ├── TestAuthentication.java
│               │               │   ├── TestBasic.java
│               │               │   ├── TestCausalCluster.java
│               │               │   ├── TestMounting.java
│               │               │   └── TestUpgrade.java
│               │               ├── neo4jadmin
│               │               │   ├── TestAdminBasic.java
│               │               │   ├── TestBackupRestore.java
│               │               │   ├── TestBackupRestore44.java
│               │               │   ├── TestDumpLoad.java
│               │               │   ├── TestDumpLoad44.java
│               │               │   └── TestReport.java
│               │               ├── TestDeprecationWarning.java
│               │               ├── TestDockerComposeSecrets.java
│               │               └── utils
│               │                   ├── DatabaseIO.java
│               │                   ├── HostFileHttpHandler.java
│               │                   ├── HttpServerTestExtension.java
│               │                   ├── Neo4jVersion.java
│               │                   ├── Neo4jVersionTest.java
│               │                   ├── Network.java
│               │                   ├── SetContainerUser.java
│               │                   ├── TemporaryFolderManager.java
│               │                   ├── TemporaryFolderManagerTest.java
│               │                   ├── TestSettings.java
│               │                   └── WaitStrategies.java
│               └── resources
│                   ├── causal-cluster-compose.yml
│                   ├── confs
│                   │   ├── before50
│                   │   │   ├── ConfsNotOverridden.conf
│                   │   │   ├── ConfsReplaced.conf
│                   │   │   ├── EnterpriseOnlyNotOverwritten.conf
│                   │   │   ├── EnvVarsOverride.conf
│                   │   │   ├── ExtendedConf.conf
│                   │   │   ├── InvalidExtendedConf.conf
│                   │   │   ├── JvmAdditionalNotOverridden.conf
│                   │   │   ├── NoNewline.conf
│                   │   │   └── ReadConf.conf
│                   │   ├── ConfsNotOverridden.conf
│                   │   ├── ConfsReplaced.conf
│                   │   ├── EnterpriseOnlyNotOverwritten.conf
│                   │   ├── EnvVarsOverride.conf
│                   │   ├── ExtendedConf.conf
│                   │   ├── InvalidExtendedConf.conf
│                   │   ├── JvmAdditionalNotOverridden.conf
│                   │   ├── NoNewline.conf
│                   │   └── ReadConf.conf
│                   ├── dockersecrets
│                   │   ├── container-compose-with-incorrect-secrets.yml
│                   │   ├── container-compose-with-secrets-override.yml
│                   │   ├── container-compose-with-secrets.yml
│                   │   ├── simple-container-compose-with-external-file-var.yml
│                   │   └── simple-container-compose.yml
│                   ├── ha-cluster-compose.yml
│                   └── stubplugin
│                       └── myPlugin.jar
├── pyproject.toml
├── README.md
├── src
│   ├── crawl4ai_mcp.py
│   └── utils.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/docker-image-src/3.2/docker-entrypoint.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash -eu
  2 | 
  3 | cmd="$1"
  4 | 
  5 | function running_as_root
  6 | {
  7 |     test "$(id -u)" = "0"
  8 | }
  9 | 
 10 | # If we're running as root, then run as the neo4j user. Otherwise
 11 | # docker is running with --user and we simply use that user.  Note
 12 | # that su-exec, despite its name, does not replicate the functionality
 13 | # of exec, so we need to use both
 14 | if running_as_root; then
 15 |   userid="neo4j"
 16 |   groupid="neo4j"
 17 |   exec_cmd="exec su-exec neo4j"
 18 | else
 19 |   userid="$(id -u)"
 20 |   groupid="$(id -g)"
 21 |   exec_cmd="exec"
 22 | fi
 23 | readonly userid
 24 | readonly groupid
 25 | readonly exec_cmd
 26 | 
 27 | # Need to chown the home directory - but a user might have mounted a
 28 | # volume here (notably a conf volume). So take care not to chown
 29 | # volumes (stuff not owned by neo4j)
 30 | if running_as_root; then
 31 |   # Non-recursive chown for the base directory
 32 |   chown "${userid}":"${groupid}" "${NEO4J_HOME}"
 33 |   chmod 700 "${NEO4J_HOME}"
 34 | fi
 35 | 
 36 | while IFS= read -r -d '' dir
 37 | do
 38 |   if running_as_root && [[ "$(stat -c %U "${dir}")" = "neo4j" ]]; then
 39 |     # Using mindepth 1 to avoid the base directory here so recursive is OK
 40 |     chown -R "${userid}":"${groupid}" "${dir}"
 41 |     chmod -R 700 "${dir}"
 42 |   fi
 43 | done <   <(find "${NEO4J_HOME}" -type d -mindepth 1 -maxdepth 1 -print0)
 44 | 
 45 | # Data dir is chowned later
 46 | 
 47 | if [ "${cmd}" == "dump-config" ]; then
 48 |   if [ -d /conf ]; then
 49 |     ${exec_cmd} cp --recursive "${NEO4J_HOME}"/conf/* /conf
 50 |     exit 0
 51 |   else
 52 |     echo >&2 "You must provide a /conf volume"
 53 |     exit 1
 54 |   fi
 55 | fi
 56 | 
 57 | # Env variable naming convention:
 58 | # - prefix NEO4J_
 59 | # - double underscore char '__' instead of single underscore '_' char in the setting name
 60 | # - underscore char '_' instead of dot '.' char in the setting name
 61 | # Example:
 62 | # NEO4J_dbms_tx__log_rotation_retention__policy env variable to set
 63 | #       dbms.tx_log.rotation.retention_policy setting
 64 | 
 65 | # Backward compatibility - map old hardcoded env variables into new naming convention (if they aren't set already)
 66 | # Set some to default values if unset
 67 | : ${NEO4J_dbms_tx__log_rotation_retention__policy:=${NEO4J_dbms_txLog_rotation_retentionPolicy:-"100M size"}}
 68 | : ${NEO4J_wrapper_java_additional:=${NEO4J_UDC_SOURCE:-"-Dneo4j.ext.udc.source=docker"}}
 69 | : ${NEO4J_dbms_memory_heap_initial__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}}
 70 | : ${NEO4J_dbms_memory_heap_max__size:=${NEO4J_dbms_memory_heap_maxSize:-"512M"}}
 71 | : ${NEO4J_dbms_unmanaged__extension__classes:=${NEO4J_dbms_unmanagedExtensionClasses:-}}
 72 | : ${NEO4J_dbms_allow__format__migration:=${NEO4J_dbms_allowFormatMigration:-}}
 73 | : ${NEO4J_dbms_connectors_default__advertised__address:=${NEO4J_dbms_connectors_defaultAdvertisedAddress:-}}
 74 | : ${NEO4J_ha_server__id:=${NEO4J_ha_serverId:-}}
 75 | : ${NEO4J_ha_initial__hosts:=${NEO4J_ha_initialHosts:-}}
 76 | : ${NEO4J_causal__clustering_expected__core__cluster__size:=${NEO4J_causalClustering_expectedCoreClusterSize:-}}
 77 | : ${NEO4J_causal__clustering_initial__discovery__members:=${NEO4J_causalClustering_initialDiscoveryMembers:-}}
 78 | : ${NEO4J_causal__clustering_discovery__listen__address:=${NEO4J_causalClustering_discoveryListenAddress:-"0.0.0.0:5000"}}
 79 | : ${NEO4J_causal__clustering_discovery__advertised__address:=${NEO4J_causalClustering_discoveryAdvertisedAddress:-"$(hostname):5000"}}
 80 | : ${NEO4J_causal__clustering_transaction__listen__address:=${NEO4J_causalClustering_transactionListenAddress:-"0.0.0.0:6000"}}
 81 | : ${NEO4J_causal__clustering_transaction__advertised__address:=${NEO4J_causalClustering_transactionAdvertisedAddress:-"$(hostname):6000"}}
 82 | : ${NEO4J_causal__clustering_raft__listen__address:=${NEO4J_causalClustering_raftListenAddress:-"0.0.0.0:7000"}}
 83 | : ${NEO4J_causal__clustering_raft__advertised__address:=${NEO4J_causalClustering_raftAdvertisedAddress:-"$(hostname):7000"}}
 84 | 
 85 | : ${NEO4J_dbms_connectors_default__listen__address:="0.0.0.0"}
 86 | : ${NEO4J_dbms_connector_http_listen__address:="0.0.0.0:7474"}
 87 | : ${NEO4J_dbms_connector_https_listen__address:="0.0.0.0:7473"}
 88 | : ${NEO4J_dbms_connector_bolt_listen__address:="0.0.0.0:7687"}
 89 | : ${NEO4J_ha_host_coordination:="$(hostname):5001"}
 90 | : ${NEO4J_ha_host_data:="$(hostname):6001"}
 91 | 
 92 | # unset old hardcoded unsupported env variables
 93 | unset NEO4J_dbms_txLog_rotation_retentionPolicy NEO4J_UDC_SOURCE \
 94 |     NEO4J_dbms_memory_heap_maxSize NEO4J_dbms_memory_heap_maxSize \
 95 |     NEO4J_dbms_unmanagedExtensionClasses NEO4J_dbms_allowFormatMigration \
 96 |     NEO4J_dbms_connectors_defaultAdvertisedAddress NEO4J_ha_serverId \
 97 |     NEO4J_ha_initialHosts NEO4J_causalClustering_expectedCoreClusterSize \
 98 |     NEO4J_causalClustering_initialDiscoveryMembers \
 99 |     NEO4J_causalClustering_discoveryListenAddress \
100 |     NEO4J_causalClustering_discoveryAdvertisedAddress \
101 |     NEO4J_causalClustering_transactionListenAddress \
102 |     NEO4J_causalClustering_transactionAdvertisedAddress \
103 |     NEO4J_causalClustering_raftListenAddress \
104 |     NEO4J_causalClustering_raftAdvertisedAddress
105 | 
106 | if [ -d /conf ]; then
107 |     find /conf -type f -exec cp {} "${NEO4J_HOME}"/conf \;
108 | fi
109 | 
110 | if [ -d /ssl ]; then
111 |     NEO4J_dbms_directories_certificates="/ssl"
112 | fi
113 | 
114 | if [ -d /plugins ]; then
115 |     NEO4J_dbms_directories_plugins="/plugins"
116 | fi
117 | 
118 | if [ -d /logs ]; then
119 |     NEO4J_dbms_directories_logs="/logs"
120 | fi
121 | 
122 | if [ -d /import ]; then
123 |     NEO4J_dbms_directories_import="/import"
124 | fi
125 | 
126 | if [ -d /metrics ]; then
127 |     NEO4J_dbms_directories_metrics="/metrics"
128 | fi
129 | 
130 | # set the neo4j initial password only if you run the database server
131 | if [ "${cmd}" == "neo4j" ]; then
132 |     if [ "${NEO4J_AUTH:-}" == "none" ]; then
133 |         NEO4J_dbms_security_auth__enabled=false
134 |     elif [[ "${NEO4J_AUTH:-}" == neo4j/* ]]; then
135 |         password="${NEO4J_AUTH#neo4j/}"
136 |         if [ "${password}" == "neo4j" ]; then
137 |             echo >&2 "Invalid value for password. It cannot be 'neo4j', which is the default."
138 |             exit 1
139 |         fi
140 |         # Will exit with error if users already exist (and print a message explaining that)
141 |         bin/neo4j-admin set-initial-password "${password}" || true
142 |     elif [ -n "${NEO4J_AUTH:-}" ]; then
143 |         echo >&2 "Invalid value for NEO4J_AUTH: '${NEO4J_AUTH}'"
144 |         exit 1
145 |     fi
146 | fi
147 | 
148 | # list env variables with prefix NEO4J_ and create settings from them
149 | unset NEO4J_AUTH NEO4J_SHA256 NEO4J_TARBALL
150 | for i in $( set | grep ^NEO4J_ | awk -F'=' '{print $1}' | sort -rn ); do
151 |     setting=$(echo ${i} | sed 's|^NEO4J_||' | sed 's|_|.|g' | sed 's|\.\.|_|g')
152 |     value=$(echo ${!i})
153 |     # Don't allow settings with no value or settings that start with a number (neo4j converts settings to env variables and you cannot have an env variable that starts with a number)
154 |     if [[ -n ${value} ]]; then
155 |         if [[ ! "${setting}" =~ ^[0-9]+.*$ ]]; then
156 |             if grep -q -F "${setting}=" "${NEO4J_HOME}"/conf/neo4j.conf; then
157 |                 # Remove any lines containing the setting already
158 |                 sed --in-place "/^${setting}=.*/d" "${NEO4J_HOME}"/conf/neo4j.conf
159 |             fi
160 |             # Then always append setting to file
161 |             echo "${setting}=${value}" >> "${NEO4J_HOME}"/conf/neo4j.conf
162 |         else
163 |             echo >&2 "WARNING: ${setting} not written to conf file because settings that start with a number are not permitted"
164 |         fi
165 |     fi
166 | done
167 | 
168 | # Chown the data dir now that (maybe) an initial password has been
169 | # set (this is a file in the data dir)
170 | if running_as_root; then
171 |   chmod -R 777 /data
172 |   chown -R "${userid}":"${groupid}" /data
173 | fi
174 | 
175 | # if we're running as root and the logs directory is not writable by the neo4j user, then chown it.
176 | # this situation happens if no user is passed to docker run and the /logs directory is mounted.
177 | if running_as_root && [[ "$(stat -c %U /logs)" != "neo4j" ]]; then
178 | #if [[ $(stat -c %u /logs) != $(id -u "${userid}") ]]; then
179 |     echo "/logs directory is not writable. Changing the directory owner to ${userid}:${groupid}"
180 |     # chown the log dir if it's not writable
181 |     chmod -R 777 /logs
182 |     chown -R "${userid}":"${groupid}" /logs
183 | fi
184 | 
185 | # If we're running as a non-default user and we can't write to the logs directory then user needs to change directory permissions manually.
186 | # This happens if a user is passed to docker run and an unwritable log directory is mounted.
187 | if ! running_as_root && [[ ! -w /logs ]]; then
188 |     echo "User does not have write permissions to mounted log directory."
189 |     echo "Manually grant write permissions for the directory and try again."
190 |     exit 1
191 | fi
192 | 
193 | [ -f "${EXTENSION_SCRIPT:-}" ] && . ${EXTENSION_SCRIPT}
194 | 
195 | # Use su-exec to drop privileges to neo4j user
196 | # Note that su-exec, despite its name, does not replicate the
197 | # functionality of exec, so we need to use both
198 | if [ "${cmd}" == "neo4j" ]; then
199 |   ${exec_cmd} neo4j console
200 | else
201 |   ${exec_cmd} "$@"
202 | fi
203 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/DEVELOPMENT.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Supported platforms
  2 | 
  3 | Development is tested on Ubuntu and OSX. It will probably work on other Linuxes.
  4 | 
  5 | # Prerequisites
  6 | 
  7 | ## OSX only
  8 | 
  9 | 1. install GNU Make (>=4.0)
 10 | 1. install the Docker Toolbox. See: https://docs.docker.com/install/
 11 | 
 12 | ## Linux
 13 | 
 14 | 1. install the Docker Toolbox. See https://docs.docker.com/install/
 15 | 
 16 | # Building the Image
 17 | 
 18 | There are two supported base operating systems that the docker image can be build upon:
 19 |  * debian, based off `debian:bullseye-slim`.
 20 |  * RedHat ubi9, based off `redhat/ubi9-minimal`. Only available for 4.4 onwards.
 21 | 
 22 | On top of that there is also the choice Neo4j version, and whether to build `community` or `enterprise` edition Neo4j.
 23 | 
 24 | ## Just give me the build TLDR
 25 | 
 26 | You probably want to create the neo4j image with whatever base image and community/enterprise variant you need, then tag it like a released neo4j version.
 27 | 
 28 | Here are some examples:
 29 | ```bash
 30 | # 5.9.0 community edition and debian 
 31 | NEO4JVERSION=5.9.0 make tag-debian-community
 32 | # creates tag neo4j:5.9.0-debian
 33 | 
 34 | # 4.4.20 community edition and redhat ubi9
 35 | NEO4JVERSION=4.4.20 make tag-ubi9-community
 36 | # creates tag neo4j:4.4.20-ubi9
 37 | 
 38 | # 5.2.0 enterprise edition and debian 
 39 | NEO4JVERSION=5.2.0 make tag-debian-enterprise
 40 | # creates tag neo4j:5.2.0-enterprise-debian
 41 | 
 42 | # 4.4.0 enterprise edition and redhat ubi9
 43 | NEO4JVERSION=4.4.0 make tag-ubi9-enterprise
 44 | # creates tag neo4j:4.4.0-enterprise-ubi9
 45 | ```
 46 | 
 47 | ## The build script
 48 | 
 49 | The build script [build-docker-image.sh](./build-docker-image.sh) will take these options and produce a Neo4j image and a neo4j-admin image, for the combination you request.
 50 | For example:
 51 | ```bash
 52 | #  debian based 4.4.22 community edition:
 53 | ./build-docker-image.sh 4.4.22 community debian
 54 | #  redhat-ubi9 based 5.9.0 enterprise edition:
 55 | ./build-docker-image.sh 5.9.0 enterprise ubi9
 56 | ```
 57 | The make script will automatically download the source files needed to build the images.
 58 | You just need to specify the **full** Neo4j version including major, minor and patch numbers.
 59 | 
 60 | The source code (entrypoint, Dockerfile and so on) is outputted into the `build/<base OS>/coredb/<edition>` and `build/<base OS>/neo4j-admin/<edition>` folders.
 61 | 
 62 | The resulting images will have a randomly generated tag, which is written into the files `build/<base OS>/coredb/.image-id-<edition>` and `build/<base OS>/neo4j-admin/.image-id-<edition>`.
 63 | 
 64 | ## Using the Convenience Makefile
 65 | 
 66 | The [Makefile](./Makefile) is a wrapper around the [build-docker-image.sh](./build-docker-image.sh).
 67 | It mostly just adds extra functionality to help you build lots of images at once, or does extra steps like tagging, testing and generating release files.
 68 | 
 69 | The four actions it can do are:
 70 | * `build`
 71 | * `test`
 72 | * `tag`
 73 | * `package`
 74 | 
 75 | For each action, it can be broken down by base image and community/enterprise type.
 76 | For example `build`, has the following make targets:
 77 | * `build`. Builds *every* variant.
 78 | * `build-debian`. Builds debian community and enterprise.
 79 | * `build-ubi9`. Builds redhat-ubi9 community and enterprise.
 80 | * `build-debian-community`
 81 | * `build-debian-enterprise`
 82 | * `build-ubi9-community`
 83 | * `build-ubi9-enterprise`
 84 | 
 85 | The other actions have the same targets.
 86 | 
 87 | This is an example of calling one of the build targets:
 88 | ```bash
 89 | NEO4JVERSION=4.4.4 make clean build-debian
 90 | ```
 91 | This will build community and enterprise, coredb and neo4j-admin, all based on debian.
 92 | 
 93 | To build and then tag all debian neo4j images, use `tag`. For example:
 94 | ```bash
 95 | NEO4JVERSION=4.4.4 make clean tag-debian
 96 | ```
 97 | 
 98 | ## Building ARM64 based images
 99 | 
100 | From Neo4j 4.4.0 onwards, the Neo4j image should be buildable on any architecture using the same build commands as [Building the Image](#building-the-image).
101 | 
102 | ### Building ARM versions before 4.4
103 | Earlier versions of Neo4j are no longer under active development and have not been tested on ARM architectures, even when those versions were under development.
104 | 
105 | It is strongly advised that you use 4.4.0 or later on an ARM system.
106 | 
107 | If you really must use an unsupported Neo4j version then in your clone of this repository, `git checkout` tag `neo4j-4.3.23` and follow development instructions there.
108 | https://github.com/neo4j/docker-neo4j/blob/neo4j-4.3.23/DEVELOPMENT.md#building-arm64-based-images
109 | 
110 | 
111 | ## If the Neo4j Version is not Publicly Available
112 | 
113 | The make script cannot automatically download unreleased source files, so you need to manually download them before building the images.
114 | 
115 | 1. Assuming you cloned this repository to `$NEO4J_DOCKER_ROOT`, 
116 | download the community and enterprise unix tar.gz files from the `packaging` build in our pipeline, and copy them to `$NEO4J_DOCKER_ROOT/in`.
117 | 1. Run the make script setting `NEO4JVERSION` to the version number in the files downloaded into the `in/` folder.
118 | 
119 | For example: 
120 | 
121 | ```bash
122 | $ cd $NEO4J_DOCKER_ROOT
123 | $ ls $NEO4J_DOCKER_ROOT/in
124 |   neo4j-community-4.0.0-alpha05-unix.tar.gz  neo4j-enterprise-4.0.0-alpha05-unix.tar.gz
125 | 
126 | $ NEO4JVERSION=4.0.0-alpha05 make clean build
127 | ``` 
128 | 
129 | ### If building an image from your local Neo4j repository
130 | 
131 | This isn't recommended since you will need to package your Neo4j tar with the browser so that neo4j will be responsive on 7474 and 7687.
132 | 
133 | 1. Clone the Neo4j github repository and checkout the branch you want.
134 | 3. Run `mvn install` plus whatever maven build flags you like. This should install the latest neo4j jars into the maven cache.
135 | 4. Copy the community and enterprise tar.gz files to `$NEO4J_DOCKER_ROOT/in`.
136 | 5. Use the `NEO4JVERSION` that is in the pom file of your Neo4j repository clone to build the docker image, e.g.:
137 | ```shell
138 | $ NEO4JVERSION=5.5.0-SNAPSHOT make clean build
139 | ```
140 | 
141 | # Running the Tests
142 | 
143 | The tests are written in java, and require Maven plus JDK 17 (any JDK distributions should work, we use OpenJDK).
144 | 
145 | The tests require some information about the image before they can test it. 
146 | These can be passed as an environment variable or a command line parameter when invoking maven:
147 | 
148 | 
149 | | Env Variable    | Maven parameter | Description                                                |
150 | |-----------------|-----------------|------------------------------------------------------------|
151 | | `NEO4JVERSION`  | `-Dversion`     | the Neo4j version of the image                             |
152 | | `NEO4J_IMAGE`   | `-Dimage`       | the tag of the image to test                               |
153 | | `NEO4JADMIN_IMAGE` | `-Dadminimage` | the tag of the neo4j-admin image to test           |
154 | | `NEO4J_EDITION` | `-Dedition`     | Either `community` or `enterprise` depending on the image. |
155 | 
156 | <!-- prettified with http://www.tablesgenerator.com/markdown_tables -->
157 | 
158 | ## Using Maven
159 | The Makefile can run the entire test suite.
160 | 1. Make sure `java --version` is java 17.
161 | 2. `NEO4JVERSION=<VERSION> make test-<BASE OS>` This is a make target that will run these commands:
162 | ```bash
163 | mvn test -Dimage=$(cat build/<BASE OS>/coredb/.image-id-enterprise) -Dadminimage=$(cat build/<BASE OS>/neo4j-admin/.image-id-enterprise) -Dedition=enterprise -Dversion=${NEO4JVERSION}
164 | mvn test -Dimage=$(cat build/<BASE OS>/coredb/.image-id-community) -Dadminimage=$(cat build/<BASE OS>/neo4j-admin/.image-id-community) -Dedition=community -Dversion=${NEO4JVERSION}
165 | ```
166 | 
167 | ## In Intellij
168 | 
169 | 1. Make sure the project SDK is java 17.
170 | 3. Install the [EnvFile](https://plugins.jetbrains.com/plugin/7861-envfile) Intellij plugin.
171 | 5. Under Run Configurations edit the Template JUnit configuration:
172 |    1. Select the "EnvFile" tab
173 |    2. Make sure "Enable EnvFile" is checked.
174 |    3. Click the `+` then click to add a `.env` file.
175 |    4. In the file selection box select `./build/<BASE OS>/devenv-enterprise.env` or `./build/<BASE OS>/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.
176 |    5. Rebuilding the Neo4j image will regenerate the `.env` files, so you don't need to worry about keeping the environment up to date.
177 | 
178 | You should now be able to run unit tests straight from the IDE.
179 | 
180 | 
181 | ## Running with podman
182 | 
183 | Tests in this module are using testcontainers. The framework expects you to have docker available on your system.
184 | And there are some issues like described here: https://github.com/testcontainers/testcontainers-java/issues/2088
185 | 
186 | TLDR on what you need to do to be able to use podman:
187 | 
188 | 1. Make sure you have podman service running. For example: ```podman system service --time=0 unix:///tmp/podman.sock```
189 | 
190 | 2. Add those environment variables:
191 | ```
192 | DOCKER_HOST=unix:///tmp/podman.sock;
193 | TESTCONTAINERS_RYUK_DISABLED=true;
194 | TESTCONTAINERS_CHECKS_DISABLE=true 
195 | ```
196 | 
197 | # Troubleshooting
198 | ## cannot find symbol `com.sun.security.auth.module.UnixSystem`
199 | 
200 | This can happen if you switch from java 17 to java 11 (or the other way) and then try to rebuild the tests in Intellij.
201 | 
202 | Check that the `java.version` property in the [pom.xml file](../master/pom.xml) is set to 17.
203 | 
204 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/plugins/TestSemVerPluginMatching.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb.plugins;
  2 | 
  3 | import com.neo4j.docker.utils.DatabaseIO;
  4 | import com.neo4j.docker.utils.HttpServerTestExtension;
  5 | import com.neo4j.docker.utils.Neo4jVersion;
  6 | import com.neo4j.docker.utils.TemporaryFolderManager;
  7 | import com.neo4j.docker.utils.TestSettings;
  8 | import com.neo4j.docker.utils.WaitStrategies;
  9 | import org.junit.jupiter.api.Assertions;
 10 | import org.junit.jupiter.api.Test;
 11 | import org.junit.jupiter.api.extension.RegisterExtension;
 12 | import org.slf4j.Logger;
 13 | import org.slf4j.LoggerFactory;
 14 | import org.testcontainers.Testcontainers;
 15 | import org.testcontainers.containers.Container;
 16 | import org.testcontainers.containers.GenericContainer;
 17 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 18 | 
 19 | import java.nio.file.Path;
 20 | import java.util.ArrayList;
 21 | import java.util.HashMap;
 22 | import java.util.List;
 23 | import java.util.Map;
 24 | import java.util.stream.Collectors;
 25 | 
 26 | import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION;
 27 | 
 28 | public class TestSemVerPluginMatching
 29 | {
 30 |     private static final String DB_USER = "neo4j";
 31 |     private static final String DB_PASSWORD = "qualityPassword123";
 32 |     private final Logger log = LoggerFactory.getLogger(TestSemVerPluginMatching.class);
 33 | 
 34 |     @RegisterExtension
 35 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 36 |     @RegisterExtension
 37 |     public HttpServerTestExtension httpServer = new HttpServerTestExtension();
 38 |     StubPluginHelper stubPluginHelper = new StubPluginHelper(httpServer);
 39 | 
 40 | 
 41 |     private GenericContainer<?> createContainerWithTestPlugin()
 42 |     {
 43 |         Testcontainers.exposeHostPorts( httpServer.PORT );
 44 |         GenericContainer<?> container = new GenericContainer<>( TestSettings.IMAGE_ID );
 45 | 
 46 |         container.withEnv( "NEO4J_AUTH", DB_USER + "/" + DB_PASSWORD )
 47 |                 .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 48 |                 .withEnv( "NEO4J_DEBUG", "yes" )
 49 |                 .withEnv( Neo4jPluginEnv.get(), "[\"" + StubPluginHelper.PLUGIN_ENV_NAME + "\"]" )
 50 |                 .withExposedPorts( 7474, 7687 )
 51 |                 .withLogConsumer( new Slf4jLogConsumer( log ) )
 52 |                 .waitingFor( WaitStrategies.waitForNeo4jReady(DB_PASSWORD));
 53 |         return container;
 54 |     }
 55 | 
 56 |     @Test
 57 |     void testSemanticVersioningLogic() throws Exception
 58 |     {
 59 |         // testing common neo4j name variants
 60 |         List<String> neo4jVersions = new ArrayList<String>()
 61 |         {{
 62 |             add( NEO4J_VERSION.toReleaseString() );
 63 |             add( NEO4J_VERSION.toReleaseString() + "-12345" );
 64 |         }};
 65 | 
 66 |         List<String> matchingCases = new ArrayList<String>()
 67 |         {{
 68 |             add( NEO4J_VERSION.toReleaseString() );
 69 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor)+".x" );
 70 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor)+".*" );
 71 |             add( NEO4J_VERSION.major + ".x.x" );
 72 |             add( NEO4J_VERSION.major + ".*.*" );
 73 |             add( "x.x.x" );
 74 |             add( "*.*.*" );
 75 |         }};
 76 | 
 77 |         List<String> nonMatchingCases = new ArrayList<String>()
 78 |         {{
 79 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major+1, NEO4J_VERSION.minor)+".x" );
 80 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major-1, NEO4J_VERSION.minor)+".x" );
 81 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor+1)+".x" );
 82 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor-1)+".x" );
 83 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major+1, NEO4J_VERSION.minor)+".*" );
 84 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major-1, NEO4J_VERSION.minor)+".*" );
 85 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor+1)+".*" );
 86 |             add( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor-1)+".*" );
 87 |         }};
 88 | 
 89 |         // Asserting every test case means that if there's a failure, all further tests won't run.
 90 |         // Instead we're running all tests and saving any failed cases for reporting at the end of the test.
 91 |         List<String> failedTests = new ArrayList<String>();
 92 | 
 93 |         try ( GenericContainer container = createContainerWithTestPlugin() )
 94 |         {
 95 |             container.withEnv( Neo4jPluginEnv.get(), "" ); // don't need the _testing plugin for this
 96 |             container.start();
 97 | 
 98 |             String semverQuery = "echo \"{\\\"neo4j\\\":\\\"%s\\\"}\" | " +
 99 |                     "jq -L/startup --raw-output \"import \\\"semver\\\" as lib; " +
100 |                     ".neo4j | lib::semver(\\\"%s\\\")\"";
101 |             for ( String verToBeMatched : neo4jVersions )
102 |             {
103 |                 for ( String verRegex : matchingCases )
104 |                 {
105 |                     Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, verRegex, verToBeMatched ) );
106 |                     if ( !out.getStdout().trim().equals( "true" ) )
107 |                     {
108 |                         failedTests.add( String.format( "%s should match %s but did not", verRegex, verToBeMatched ) );
109 |                     }
110 |                 }
111 |                 for ( String verRegex : nonMatchingCases )
112 |                 {
113 |                     Container.ExecResult out = container.execInContainer( "sh", "-c", String.format( semverQuery, verRegex, verToBeMatched ) );
114 |                     if ( !out.getStdout().trim().equals( "false" ) )
115 |                     {
116 |                         failedTests.add( String.format( "%s should NOT match %s but did", verRegex, verToBeMatched ) );
117 |                     }
118 |                 }
119 |             }
120 |             if ( !failedTests.isEmpty() )
121 |             {
122 |                 Assertions.fail( failedTests.stream().collect( Collectors.joining( "\n" ) ) );
123 |             }
124 |         }
125 |     }
126 | 
127 |     @Test
128 |     void testSemanticVersioningPlugin_catchesMatchWithX() throws Exception
129 |     {
130 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
131 |         stubPluginHelper.createStubPluginForVersion(pluginsDir,
132 |             Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor)+".x");
133 |         try ( GenericContainer container = createContainerWithTestPlugin() )
134 |         {
135 |             container.start();
136 |             DatabaseIO db = new DatabaseIO( container );
137 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
138 |         }
139 |     }
140 | 
141 |     @Test
142 |     void testSemanticVersioningPlugin_catchesMatchWithStar() throws Exception
143 |     {
144 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
145 |         stubPluginHelper.createStubPluginForVersion(pluginsDir,
146 |             Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor)+".*");
147 |         try ( GenericContainer container = createContainerWithTestPlugin() )
148 |         {
149 |             container.start();
150 |             DatabaseIO db = new DatabaseIO( container );
151 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
152 |         }
153 |     }
154 | 
155 |     @Test
156 |     void testSemanticVersioningPlugin_prefersExactMatch() throws Exception
157 |     {
158 |         verifySemanticVersioningPrefersBetterMatches(new HashMap<String,String>()
159 |             {{
160 |                 put( "x.x.x", "notareal.jar" );
161 |                 put( NEO4J_VERSION.major + ".x.x", "notareal.jar" );
162 |                 put( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor) + ".x", "notareal.jar" );
163 |                 put( NEO4J_VERSION.toString(), StubPluginHelper.PLUGIN_FILENAME);
164 |             }} );
165 |     }
166 | 
167 |     @Test
168 |     void testSemanticVersioningPlugin_prefersMajorMinorMatch() throws Exception
169 |     {
170 |         verifySemanticVersioningPrefersBetterMatches(new HashMap<String,String>()
171 |             {{
172 |                 put( "x.x.x", "notareal.jar" );
173 |                 put( NEO4J_VERSION.major + ".x.x", "notareal.jar" );
174 |                 put( Neo4jVersion.makeVersionString( NEO4J_VERSION.major, NEO4J_VERSION.minor) + ".x",
175 |                      StubPluginHelper.PLUGIN_FILENAME);
176 |             }} );
177 |     }
178 | 
179 |     @Test
180 |     void testSemanticVersioningPlugin_prefersMajorMatch() throws Exception
181 |     {
182 |         verifySemanticVersioningPrefersBetterMatches(new HashMap<String,String>()
183 |             {{
184 |                 put( "x.x.x", "notareal.jar" );
185 |                 put( NEO4J_VERSION.major + ".x.x", StubPluginHelper.PLUGIN_FILENAME);
186 |             }} );
187 |     }
188 | 
189 |     void verifySemanticVersioningPrefersBetterMatches(Map<String, String> versionsInJson) throws Exception {
190 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
191 |         stubPluginHelper.createStubPluginsForVersionMapping(pluginsDir, versionsInJson);
192 |         try (GenericContainer container = createContainerWithTestPlugin()) {
193 |             container.start();
194 |             DatabaseIO db = new DatabaseIO(container);
195 |             // if semver did not pick exact version match then it will load a non-existent plugin instead and fail.
196 |             stubPluginHelper.verifyStubPluginLoaded(db, DB_USER, DB_PASSWORD);
197 |         }
198 |     }
199 | }
200 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/configurations/TestExtendedConf.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb.configurations;
  2 | 
  3 | import com.neo4j.docker.utils.Neo4jVersion;
  4 | import com.neo4j.docker.utils.SetContainerUser;
  5 | import com.neo4j.docker.utils.WaitStrategies;
  6 | import com.neo4j.docker.utils.TemporaryFolderManager;
  7 | import com.neo4j.docker.utils.TestSettings;
  8 | import org.junit.jupiter.api.Assertions;
  9 | import org.junit.jupiter.api.Assumptions;
 10 | import org.junit.jupiter.api.BeforeAll;
 11 | import org.junit.jupiter.api.extension.RegisterExtension;
 12 | import org.junit.jupiter.params.ParameterizedTest;
 13 | import org.junit.jupiter.params.provider.ValueSource;
 14 | import org.slf4j.Logger;
 15 | import org.slf4j.LoggerFactory;
 16 | import org.testcontainers.containers.BindMode;
 17 | import org.testcontainers.containers.ContainerLaunchException;
 18 | import org.testcontainers.containers.GenericContainer;
 19 | import org.testcontainers.containers.output.OutputFrame;
 20 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 21 | import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
 22 | import org.testcontainers.containers.wait.strategy.Wait;
 23 | 
 24 | import java.io.IOException;
 25 | import java.nio.file.Files;
 26 | import java.nio.file.Path;
 27 | import java.nio.file.attribute.PosixFilePermission;
 28 | import java.time.Duration;
 29 | import java.util.HashSet;
 30 | import java.util.Optional;
 31 | import java.util.regex.Matcher;
 32 | import java.util.regex.Pattern;
 33 | import java.util.stream.Stream;
 34 | 
 35 | public class TestExtendedConf
 36 | {
 37 | 	private final Logger log = LoggerFactory.getLogger( TestExtendedConf.class );
 38 |     private static Path testConfsFolder;
 39 |     private static Configuration logRotationConfig;
 40 |     @RegisterExtension
 41 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 42 | 
 43 | 	@BeforeAll
 44 | 	static void ensureFeaturePresent()
 45 | 	{
 46 | 		Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( new Neo4jVersion( 4,2,0 ) ),
 47 | 								"Extended configuration feature not available before 4.2" );
 48 | 	}
 49 | 
 50 |     @BeforeAll
 51 |     static void createVersionSpecificConfigurationSettings() {
 52 |         testConfsFolder = Configuration.getConfigurationResourcesFolder();
 53 |         logRotationConfig = Configuration.getConfigurationNameMap()
 54 |                                          .get( Setting.LOGS_GC_ROTATION_KEEPNUMBER );
 55 |     }
 56 | 
 57 | 	protected GenericContainer createContainer(String password)
 58 | 	{
 59 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )
 60 |                 .withEnv( "NEO4J_AUTH", password == null || password.isEmpty() ? "none" : "neo4j/" + password )
 61 |                 .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 62 |                 .withEnv( "EXTENDED_CONF", "yeppers" )
 63 |                 .withExposedPorts( 7474, 7687 )
 64 |                 .withLogConsumer( new Slf4jLogConsumer( log ) )
 65 |                 .waitingFor( WaitStrategies.waitForBoltReady());
 66 |        return container;
 67 |     }
 68 | 
 69 | 
 70 | 	@ParameterizedTest
 71 | 	@ValueSource(strings = {"", "supersecretpassword"})
 72 | 	public void shouldStartWithExtendedConf(String password)
 73 | 	{
 74 |         try(GenericContainer container = createContainer(password))
 75 |         {
 76 |             container.start();
 77 | 
 78 |             Assertions.assertTrue( container.isRunning() );
 79 | 			assertPasswordChangedLogIsCorrect( password, container );
 80 | 		}
 81 | 	}
 82 | 
 83 | 	private void assertPasswordChangedLogIsCorrect( String password, GenericContainer container )
 84 | 	{
 85 | 		if ( password.isEmpty()) {
 86 | 			Assertions.assertFalse( container.getLogs( OutputFrame.OutputType.STDOUT)
 87 |                                              .contains( "Changed password for user 'neo4j'." ) );
 88 | 		} else {
 89 | 			Assertions.assertTrue( container.getLogs( OutputFrame.OutputType.STDOUT)
 90 |                                             .contains( "Changed password for user 'neo4j'." ) );
 91 | 		}
 92 | 	}
 93 | 
 94 | 	@ParameterizedTest
 95 | 	@ValueSource(strings = {"", "supersecretpassword"})
 96 | 	void testReadsTheExtendedConfFile_defaultUser(String password) throws Exception
 97 | 	{
 98 | 		// set up test folders
 99 | 		Path confFolder = temporaryFolderManager.createFolder("conf");
100 | 		Path logsFolder = temporaryFolderManager.createFolder("logs");
101 | 
102 | 		// copy configuration file and set permissions
103 | 		Path confFile = testConfsFolder.resolve( "ExtendedConf.conf" );
104 | 		Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) );
105 |         chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) );
106 | 		temporaryFolderManager.setFolderOwnerToNeo4j( confFolder.resolve( "neo4j.conf" ) );
107 | 
108 | 		// start  container
109 | 		try(GenericContainer container = createContainer(password))
110 | 		{
111 | 			runContainerAndVerify( container, confFolder, logsFolder, password );
112 | 		}
113 | 	}
114 | 
115 |     @ParameterizedTest
116 |     @ValueSource( strings = {"", "supersecretpassword"} )
117 |     void testInvalidExtendedConfFile_nonRootUser( String password ) throws Exception
118 |     {
119 |         // set up test folder
120 |         Path confFolder = temporaryFolderManager.createFolder("conf");
121 | 
122 |         // copy configuration file and set permissions
123 |         Files.copy( testConfsFolder.resolve( "InvalidExtendedConf.conf" ), confFolder.resolve( "neo4j.conf" ) );
124 |         chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) );
125 | 
126 |         try(GenericContainer container = createContainer( password ))
127 |         {
128 |             SetContainerUser.nonRootUser( container );
129 |             container.withFileSystemBind( "/etc/passwd", "/etc/passwd", BindMode.READ_ONLY );
130 |             container.withFileSystemBind( "/etc/group", "/etc/group", BindMode.READ_ONLY );
131 |             temporaryFolderManager.mountHostFolderAsVolume( container, confFolder, "/conf" );
132 |             container.setStartupCheckStrategy( new OneShotStartupCheckStrategy().withTimeout( Duration.ofSeconds( 30 ) ) );
133 |             container.setWaitStrategy(
134 |                     Wait.forLogMessage( ".*this is an error message from inside neo4j config command expansion.*", 1 )
135 |                         .withStartupTimeout( Duration.ofSeconds( 30 ) ) );
136 | 
137 |             Assertions.assertThrows( ContainerLaunchException.class,
138 |                                      () -> container.start(),
139 |                                      "Container should have errored on start");
140 | 
141 |             String logs = container.getLogs();
142 |             // check that error messages from neo4j are visible in docker logs
143 |             Assertions.assertTrue( logs.contains( "Error evaluating value for setting '" + logRotationConfig.name + "'" ) );
144 |             // check that error messages from the command that failed are visible in docker logs
145 |             Assertions.assertTrue( logs.contains( "this is an error message from inside neo4j config command expansion" ) );
146 |             // check that the error is only encountered once (i.e. we quit the docker entrypoint the first time it was encountered)
147 |             Assertions.assertEquals( 1, countOccurrences( Pattern.compile( "Error evaluating value for setting" ), logs ) );
148 |         }
149 |     }
150 | 
151 | 	private int countOccurrences( Pattern pattern, String inString )
152 | 	{
153 | 		Matcher matcher = pattern.matcher( inString );
154 | 		int count = 0;
155 | 		while ( matcher.find() )
156 | 		{
157 | 			count = count + 1;
158 | 		}
159 | 		return count;
160 | 	}
161 | 
162 | 	@ParameterizedTest
163 | 	@ValueSource(strings = {"", "supersecretpassword"})
164 | 	void testReadsTheExtendedConfFile_nonRootUser(String password) throws Exception
165 | 	{
166 | 		// set up test folders
167 | 		Path confFolder = temporaryFolderManager.createFolder("conf");
168 | 		Path logsFolder = temporaryFolderManager.createFolder("logs");
169 | 
170 | 		// copy configuration file and set permissions
171 | 		Path confFile = testConfsFolder.resolve( "ExtendedConf.conf" );
172 | 		Files.copy( confFile, confFolder.resolve( "neo4j.conf" ) );
173 | 		chmodConfFilePermissions( confFolder.resolve( "neo4j.conf" ) );
174 | 
175 | 		try(GenericContainer container = createContainer(password))
176 | 		{
177 | 			SetContainerUser.nonRootUser( container );
178 | 			container.withFileSystemBind( "/etc/passwd", "/etc/passwd", BindMode.READ_ONLY );
179 | 			container.withFileSystemBind( "/etc/group", "/etc/group", BindMode.READ_ONLY );
180 | 			runContainerAndVerify( container, confFolder, logsFolder, password );
181 | 		}
182 | 	}
183 | 
184 | 	private void runContainerAndVerify(GenericContainer container, Path confFolder, Path logsFolder, String password) throws Exception
185 | 	{
186 | 		temporaryFolderManager.mountHostFolderAsVolume( container, confFolder, "/conf" );
187 | 		temporaryFolderManager.mountHostFolderAsVolume( container, logsFolder, "/logs" );
188 | 
189 | 		container.start();
190 | 
191 | 		Path debugLog = logsFolder.resolve("debug.log");
192 | 		Assertions.assertTrue(debugLog.toFile().exists(), "Did not write debug log");
193 | 
194 | 		//Check if the container reads the conf file
195 | 		Stream<String> lines = Files.lines( debugLog);
196 | 		Optional<String> isMatch = lines.filter( s -> s.contains(logRotationConfig.name + "=20")).findFirst();
197 | 		lines.close();
198 | 		Assertions.assertTrue(  isMatch.isPresent(), logRotationConfig.name+" was not set correctly");
199 | 
200 | 		//Check the password was changed if set
201 | 		assertPasswordChangedLogIsCorrect( password, container );
202 | 	}
203 | 
204 | 	private void chmodConfFilePermissions( Path file ) throws IOException
205 | 	{
206 | 
207 | 		HashSet<PosixFilePermission> permissions = new HashSet<PosixFilePermission>()
208 | 		{{
209 | 			add( PosixFilePermission.OWNER_READ );
210 | 			add( PosixFilePermission.OWNER_WRITE );
211 | 		}};
212 | 
213 | 		if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 3, 0 ) ) )
214 | 		{
215 | 			permissions.add( PosixFilePermission.GROUP_READ );
216 | 		}
217 | 		Files.setPosixFilePermissions( file, permissions );
218 | 	}
219 | }
220 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestAdminReport.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb;
  2 | 
  3 | import com.neo4j.docker.utils.DatabaseIO;
  4 | import com.neo4j.docker.utils.Neo4jVersion;
  5 | import com.neo4j.docker.utils.SetContainerUser;
  6 | import com.neo4j.docker.utils.WaitStrategies;
  7 | import com.neo4j.docker.utils.TemporaryFolderManager;
  8 | import com.neo4j.docker.utils.TestSettings;
  9 | import org.junit.jupiter.api.Assertions;
 10 | import org.junit.jupiter.api.BeforeAll;
 11 | import org.junit.jupiter.api.Test;
 12 | import org.junit.jupiter.api.extension.RegisterExtension;
 13 | import org.junit.jupiter.params.ParameterizedTest;
 14 | import org.junit.jupiter.params.provider.ValueSource;
 15 | import org.slf4j.Logger;
 16 | import org.slf4j.LoggerFactory;
 17 | import org.testcontainers.containers.Container;
 18 | import org.testcontainers.containers.ContainerLaunchException;
 19 | import org.testcontainers.containers.GenericContainer;
 20 | import org.testcontainers.containers.output.OutputFrame;
 21 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 22 | 
 23 | import java.io.File;
 24 | import java.nio.file.Files;
 25 | import java.nio.file.Path;
 26 | import java.time.Duration;
 27 | import java.util.List;
 28 | 
 29 | 
 30 | public class TestAdminReport
 31 | {
 32 |     private final Logger log = LoggerFactory.getLogger( TestAdminReport.class );
 33 |     private final String PASSWORD = "supersecretpassword";
 34 |     @RegisterExtension
 35 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 36 |     private static String reportDestinationFlag;
 37 | 
 38 |     @BeforeAll
 39 |     static void setCorrectPathFlagForVersion()
 40 |     {
 41 |         if( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ) )
 42 |         {
 43 |             reportDestinationFlag = "--to";
 44 |         }
 45 |         else
 46 |         {
 47 |             reportDestinationFlag = "--to-path";
 48 |         }
 49 |     }
 50 | 
 51 |     private GenericContainer createNeo4jContainer( boolean asCurrentUser)
 52 |     {
 53 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )
 54 |                 .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 55 |                 .withEnv( "NEO4J_AUTH", "neo4j/"+PASSWORD )
 56 |                 .withExposedPorts( 7474, 7687 )
 57 |                 .withLogConsumer( new Slf4jLogConsumer( log ) )
 58 |                 .waitingFor(WaitStrategies.waitForNeo4jReady( PASSWORD ));
 59 |         if(asCurrentUser)
 60 |         {
 61 |             SetContainerUser.nonRootUser( container );
 62 |         }
 63 |         return container;
 64 |     }
 65 | 
 66 |     @ParameterizedTest(name = "ascurrentuser_{0}")
 67 |     @ValueSource(booleans = {true, false})
 68 |     void testMountToTmpReports(boolean asCurrentUser) throws Exception
 69 |     {
 70 |         try(GenericContainer container = createNeo4jContainer(asCurrentUser))
 71 |         {
 72 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
 73 |             Path reportFolder = temporaryFolderManager.createFolderAndMountAsVolume(container, "/tmp/reports");
 74 |             container.start();
 75 |             DatabaseIO dbio = new DatabaseIO( container );
 76 |             dbio.putInitialDataIntoContainer( "neo4j", PASSWORD );
 77 | 
 78 |             Container.ExecResult execResult = container.execInContainer( "neo4j-admin-report" );
 79 |             verifyCreatesReport( reportFolder, execResult );
 80 |         }
 81 |     }
 82 | 
 83 |     @ParameterizedTest(name = "ascurrentuser_{0}")
 84 |     @ValueSource(booleans = {true, false})
 85 |     void testCanWriteReportToAnyMountedLocation_toPathWithEquals(boolean asCurrentUser) throws Exception
 86 |     {
 87 |         String reportFolderName = "reportAnywhere-"+ (asCurrentUser? "currentuser-":"defaultuser-") + "withEqualsArg-";
 88 |         verifyCanWriteToMountedLocation( asCurrentUser,
 89 |                                          reportFolderName,
 90 |                                          new String[]{"neo4j-admin-report", "--verbose", reportDestinationFlag+"=/reports"} );
 91 |     }
 92 | 
 93 |     @ParameterizedTest(name = "ascurrentuser_{0}")
 94 |     @ValueSource(booleans = {true, false})
 95 |     void testCanWriteReportToAnyMountedLocation_toPathWithSpace(boolean asCurrentUser) throws Exception
 96 |     {
 97 |         String reportFolderName = "reportAnywhere-"+ (asCurrentUser? "currentuser-":"defaultuser-") + "withSpaceArg-";
 98 |         verifyCanWriteToMountedLocation( asCurrentUser,
 99 |                                          reportFolderName,
100 |                                          new String[]{"neo4j-admin-report", "--verbose", reportDestinationFlag, "/reports"} );
101 |     }
102 | 
103 |     private void verifyCanWriteToMountedLocation(boolean asCurrentUser, String testFolderPrefix, String[] execArgs) throws Exception
104 |     {
105 |         try(GenericContainer container = createNeo4jContainer(asCurrentUser))
106 |         {
107 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
108 |             Path reportFolder = temporaryFolderManager.createFolderAndMountAsVolume(container, "/reports");
109 |             container.start();
110 |             DatabaseIO dbio = new DatabaseIO( container );
111 |             dbio.putInitialDataIntoContainer( "neo4j", PASSWORD );
112 |             Container.ExecResult execResult = container.execInContainer(execArgs);
113 |             // log exec results, because the results of an exec don't get logged automatically.
114 |             log.info( execResult.getStdout() );
115 |             log.warn( execResult.getStderr() );
116 |             verifyCreatesReport( reportFolder, execResult );
117 |         }
118 |     }
119 | 
120 |     @Test
121 |     void shouldShowNeo4jAdminHelpText_whenCMD() throws Exception
122 |     {
123 |         try(GenericContainer container = createNeo4jContainer(false))
124 |         {
125 |             container.withCommand( "neo4j-admin-report", "--help" );
126 |             WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 20 ) );
127 |             try
128 |             {
129 |                 container.start();
130 |             }
131 |             catch ( ContainerLaunchException e )
132 |             {
133 |                 // consume any failed to start exceptions
134 |                 log.warn( "Running 'neo4j-admin-report --help' caused the container to fail rather than " +
135 |                           "successfully complete. This is allowable, so the test is not going to fail." );
136 |             }
137 |             verifyHelpText( container.getLogs(OutputFrame.OutputType.STDOUT),
138 |                             container.getLogs(OutputFrame.OutputType.STDERR) );
139 |         }
140 |     }
141 | 
142 |     @Test
143 |     void shouldShowNeo4jAdminHelpText_whenEXEC() throws Exception
144 |     {
145 |         try(GenericContainer container = createNeo4jContainer(false))
146 |         {
147 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
148 |             container.start();
149 |             Container.ExecResult execResult = container.execInContainer( "neo4j-admin-report", "--help" );
150 |             // log exec results, because the results of an exec don't get logged automatically.
151 |             log.info( "STDOUT:\n" + execResult.getStdout() );
152 |             log.warn( "STDERR:\n" + execResult.getStderr() );
153 |             verifyHelpText( execResult.getStdout(), execResult.getStderr() );
154 |         }
155 |     }
156 | 
157 |     private void verifyCreatesReport( Path reportFolder,Container.ExecResult reportExecOut ) throws Exception
158 |     {
159 |         List<File> reports = Files.list( reportFolder )
160 |                                   .map( Path::toFile )
161 |                                   .filter( file -> ! file.isDirectory() )
162 |                                   .toList();
163 |         if( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ) )
164 |         {
165 |             // for some reason neo4j-admin report prints jvm details to stderr
166 |             String[] lines = reportExecOut.getStderr().split( "\n" );
167 |             Assertions.assertEquals( 1, lines.length,
168 |                                      "There were errors during report generation" );
169 |             Assertions.assertTrue( lines[0].startsWith( "Selecting JVM" ),
170 |                                    "There were unexpected error messages in the neo4j-admin report:\n"+reportExecOut.getStderr() );
171 |         }
172 |         else
173 |         {
174 |             Assertions.assertEquals( "", reportExecOut.getStderr(),
175 |                                      "There were errors during report generation" );
176 |         }
177 |         Assertions.assertEquals( 1, reports.size(), "Expected exactly 1 report to be produced" );
178 |         Assertions.assertFalse( reportExecOut.toString().contains( "No running instance of neo4j was found" ),
179 |                                 "neo4j-admin could not locate running neo4j database" );
180 |     }
181 | 
182 |     private void verifyHelpText(String stdout, String stderr)
183 |     {
184 |         // in 4.4 the help text goes in stderr
185 |         if( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ) )
186 |         {
187 |             Assertions.assertTrue( stderr.contains(
188 |                     "Produces a zip/tar of the most common information needed for remote assessments." ) );
189 |             Assertions.assertTrue( stderr.contains( "USAGE" ) );
190 |             Assertions.assertTrue( stderr.contains( "OPTIONS" ) );
191 |         }
192 |         else
193 |         {
194 |             Assertions.assertTrue( stdout.contains(
195 |                     "Produces a zip/tar of the most common information needed for remote assessments." ) );
196 |             Assertions.assertTrue( stdout.contains( "USAGE" ) );
197 |             Assertions.assertTrue( stdout.contains( "OPTIONS" ) );
198 |             Assertions.assertEquals( "", stderr, "There were errors when trying to get neo4j-admin-report help text" );
199 |         }
200 |     }
201 | }
202 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/plugins/TestBundledPluginInstallation.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb.plugins;
  2 | 
  3 | import com.neo4j.docker.utils.DatabaseIO;
  4 | import com.neo4j.docker.utils.Neo4jVersion;
  5 | import com.neo4j.docker.utils.TemporaryFolderManager;
  6 | import com.neo4j.docker.utils.TestSettings;
  7 | import com.neo4j.docker.utils.WaitStrategies;
  8 | import org.jetbrains.annotations.Nullable;
  9 | import org.junit.jupiter.api.Assertions;
 10 | import org.junit.jupiter.api.Assumptions;
 11 | import org.junit.jupiter.api.Tag;
 12 | import org.junit.jupiter.api.Test;
 13 | import org.junit.jupiter.api.extension.RegisterExtension;
 14 | import org.junit.jupiter.params.ParameterizedTest;
 15 | import org.junit.jupiter.params.provider.Arguments;
 16 | import org.junit.jupiter.params.provider.MethodSource;
 17 | import org.slf4j.Logger;
 18 | import org.slf4j.LoggerFactory;
 19 | import org.testcontainers.containers.Container;
 20 | import org.testcontainers.containers.ContainerLaunchException;
 21 | import org.testcontainers.containers.GenericContainer;
 22 | import org.testcontainers.containers.output.OutputFrame;
 23 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 24 | import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
 25 | 
 26 | import java.nio.file.Files;
 27 | import java.nio.file.Path;
 28 | import java.time.Duration;
 29 | import java.util.List;
 30 | import java.util.stream.Collectors;
 31 | import java.util.stream.Stream;
 32 | 
 33 | @Tag("BundleTest")
 34 | public class TestBundledPluginInstallation
 35 | {
 36 |     private final Logger log = LoggerFactory.getLogger( TestBundledPluginInstallation.class );
 37 |     @RegisterExtension
 38 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 39 | 
 40 |     static class BundledPlugin
 41 |     {
 42 |         private final String name;
 43 |         private final Neo4jVersion bundledSince;
 44 |         private final Neo4jVersion bundledUntil;
 45 |         private final boolean isEnterpriseOnly;
 46 | 
 47 |         public BundledPlugin(String name, Neo4jVersion bundledSince, @Nullable Neo4jVersion bundledUntil, boolean isEnterpriseOnly)
 48 |         {
 49 |             this.name = name;
 50 |             this.bundledSince = bundledSince;
 51 |             this.bundledUntil = bundledUntil;
 52 |             this.isEnterpriseOnly = isEnterpriseOnly;
 53 |         }
 54 | 
 55 |         public boolean shouldBePresentInImage()
 56 |         {
 57 |             boolean shouldBeBundled = TestSettings.NEO4J_VERSION.isAtLeastVersion( bundledSince );
 58 |             if(isEnterpriseOnly)
 59 |             {
 60 |                 shouldBeBundled = shouldBeBundled && (TestSettings.EDITION == TestSettings.Edition.ENTERPRISE);
 61 |             }
 62 |             if(bundledUntil != null)
 63 |             {
 64 |                 shouldBeBundled = shouldBeBundled && TestSettings.NEO4J_VERSION.isOlderThan( bundledUntil );
 65 |             }
 66 |             return shouldBeBundled;
 67 |         }
 68 | 
 69 |         @Override
 70 |         public String toString()
 71 |         {
 72 |             return "BundledPlugin " + name;
 73 |         }
 74 |     }
 75 | 
 76 |     private static final BundledPlugin APOC = new BundledPlugin("apoc",
 77 |             new Neo4jVersion(5, 0, 0), null,false);
 78 |     private static final BundledPlugin APOC_CORE = new BundledPlugin("apoc-core",
 79 |             new Neo4jVersion(4, 3, 15),
 80 |             new Neo4jVersion(5, 0, 0), false);
 81 |     private static final BundledPlugin BLOOM = new BundledPlugin("bloom",
 82 |             Neo4jVersion.NEO4J_VERSION_440, null, true);
 83 |     private static final BundledPlugin GDS = new BundledPlugin("graph-data-science",
 84 |             Neo4jVersion.NEO4J_VERSION_440, null, true );
 85 |     private static final BundledPlugin GENAI = new BundledPlugin("genai",
 86 |             new Neo4jVersion(5, 18, 0), null, false);
 87 | 
 88 |     static Stream<Arguments> bundledPluginsArgs() {
 89 |         return Stream.of(
 90 |                 Arguments.arguments(APOC_CORE),
 91 |                 Arguments.arguments(APOC),
 92 |                  Arguments.arguments(GDS),
 93 |                 Arguments.arguments(BLOOM),
 94 |                 Arguments.arguments(GENAI)
 95 |         );
 96 |     }
 97 | 
 98 |     private GenericContainer createContainer()
 99 |     {
100 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
101 |         container.withEnv( "NEO4J_AUTH", "none" )
102 |                  .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
103 |                  .withEnv("NEO4J_DEBUG", "yes")
104 |                  .withExposedPorts( 7474, 7687 )
105 |                  .withLogConsumer( new Slf4jLogConsumer( log ) )
106 |                  .waitingFor( WaitStrategies.waitForBoltReady() );
107 |         return container;
108 |     }
109 | 
110 |     private GenericContainer createContainerWithBundledPlugin(BundledPlugin plugin)
111 |     {
112 |         return createContainer().withEnv( Neo4jPluginEnv.get(), "[\"" +plugin.name+ "\"]" );
113 |     }
114 | 
115 |     @ParameterizedTest(name = "testBundledPlugin_{0}")
116 |     @MethodSource("bundledPluginsArgs")
117 |     public void testBundledPlugin(BundledPlugin plugin) throws Exception
118 |     {
119 |         Assumptions.assumeTrue(plugin.shouldBePresentInImage(),
120 |                 "test only applies when the plugin "+plugin.name+" is present");
121 | 
122 |         GenericContainer container = null;
123 |         Path pluginsMount = null;
124 |         try
125 |         {
126 |             container = createContainerWithBundledPlugin(plugin);
127 |             pluginsMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/plugins");
128 |             container.start();
129 |             DatabaseIO dbio = new DatabaseIO( container );
130 |             dbio.putInitialDataIntoContainer( "neo4j", "none" );
131 |         }
132 |         catch(ContainerLaunchException e)
133 |         {
134 |             // we don't want this test to depend on the plugins actually working (that's outside the scope of
135 |             // the docker tests), so we have to be robust to the container failing to start.
136 |             log.error( String.format("The bundled %s plugin caused Neo4j to fail to start.", plugin.name) );
137 |         }
138 |         finally
139 |         {
140 |             // verify the plugins were loaded.
141 |             // This is done in the finally block because after stopping the container, the stdout cannot be retrieved.
142 |             if (pluginsMount != null)
143 |             {
144 |                 List<String> plugins = Files.list(pluginsMount).map( fname -> fname.getFileName().toString() )
145 |                                             .filter( fname -> fname.endsWith( ".jar" ) )
146 |                                             .collect(Collectors.toList());
147 |                 Assertions.assertTrue(plugins.size() == 1, "more than one plugin was loaded" );
148 |                 Assertions.assertTrue( plugins.get( 0 ).contains( plugin.name ) );
149 |                 // Verify from container logs, that the plugins were loaded locally rather than downloaded.
150 |                 String logs = container.getLogs( OutputFrame.OutputType.STDOUT);
151 |                 String errlogs = container.getLogs( OutputFrame.OutputType.STDERR);
152 |                 Assertions.assertTrue(
153 |                         Stream.of(logs.split( "\n" ))
154 |                               .anyMatch( line -> line.matches( "Installing Plugin '" + plugin.name + "' from /var/lib/neo4j/.*" ) ),
155 |                         "Plugin was not installed from neo4j home");
156 |             }
157 |             if(container !=null)
158 |             {
159 |                 container.stop();
160 |             }
161 |             else
162 |             {
163 |                 Assertions.fail("Test failed before container could even be initialised");
164 |             }
165 |         }
166 |     }
167 | 
168 |     @Test
169 |     void testPluginLoadsWithAuthentication() throws Exception
170 |     {
171 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ) );
172 | 
173 |         final String PASSWORD = "12345678";
174 | 
175 |         try( GenericContainer container = createContainerWithBundledPlugin(BLOOM))
176 |         {
177 |             container.withEnv( "NEO4J_AUTH", "neo4j/"+PASSWORD )
178 |                      .withEnv( "NEO4J_dbms_bloom_license__file", "/licenses/bloom.license" );
179 |             // mounting logs because it's useful for debugging
180 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
181 |             Path licenseFolder = temporaryFolderManager.createFolderAndMountAsVolume(container, "/licenses");
182 |             Files.writeString( licenseFolder.resolve("bloom.license"), "notareallicense" );
183 |             // make sure the container successfully starts and we can write to it without getting authentication errors
184 |             container.start();
185 |             DatabaseIO dbio = new DatabaseIO( container );
186 |             dbio.putInitialDataIntoContainer( "neo4j", PASSWORD );
187 |         }
188 |     }
189 | 
190 |     @Test
191 |     void testBrowserListensOn7474() throws Exception
192 |     {
193 |         try(GenericContainer container = createContainer())
194 |         {
195 |             container.waitingFor( new HttpWaitStrategy()
196 |                                           .forPort(7474)
197 |                                           .forStatusCode(200)
198 |                                           .withStartupTimeout(Duration.ofSeconds(60)) );
199 |             container.start();
200 |             Assertions.assertTrue( container.isRunning() );
201 |             Container.ExecResult r = container.execInContainer( "wget", "-q", "-O", "-", "http://localhost:7474/browser/" );
202 |             Assertions.assertEquals( 0, r.getExitCode(), "Did not get http response from browser");
203 |             Assertions.assertFalse( r.getStdout().isEmpty(), "HTTP response from browser was empty." );
204 |             Assertions.assertTrue( r.getStdout().contains( "Neo4j Browser" ),
205 |                                    "HTTP response from browser did not contain expected information.\n"+r.getStdout());
206 |         }
207 |     }
208 | }
209 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/TestDockerComposeSecrets.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker;
  2 | 
  3 | import com.github.dockerjava.api.DockerClient;
  4 | import com.neo4j.docker.coredb.configurations.Configuration;
  5 | import com.neo4j.docker.coredb.configurations.Setting;
  6 | import com.neo4j.docker.utils.DatabaseIO;
  7 | import com.neo4j.docker.utils.TemporaryFolderManager;
  8 | import com.neo4j.docker.utils.TestSettings;
  9 | import org.junit.jupiter.api.Assertions;
 10 | import org.junit.jupiter.api.Assumptions;
 11 | import org.junit.jupiter.api.BeforeAll;
 12 | import org.junit.jupiter.api.Test;
 13 | import org.junit.jupiter.api.extension.RegisterExtension;
 14 | import org.slf4j.Logger;
 15 | import org.slf4j.LoggerFactory;
 16 | import org.testcontainers.DockerClientFactory;
 17 | import org.testcontainers.containers.ComposeContainer;
 18 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 19 | import org.testcontainers.containers.output.ToStringConsumer;
 20 | 
 21 | import java.io.File;
 22 | import java.io.IOException;
 23 | import java.nio.file.Files;
 24 | import java.nio.file.Path;
 25 | import java.nio.file.Paths;
 26 | import java.nio.file.attribute.PosixFilePermissions;
 27 | 
 28 | import static com.neo4j.docker.utils.WaitStrategies.waitForBoltReady;
 29 | 
 30 | public class TestDockerComposeSecrets
 31 | {
 32 |     private final Logger log = LoggerFactory.getLogger( TestDockerComposeSecrets.class );
 33 | 
 34 |     private static final int DEFAULT_BOLT_PORT = 7687;
 35 |     private static final int DEFAULT_HTTP_PORT = 7474;
 36 |     private static final Path TEST_RESOURCES_PATH = Paths.get( "src", "test", "resources", "dockersecrets" );
 37 | 
 38 |     @RegisterExtension
 39 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 40 | 
 41 |     @BeforeAll
 42 |     public static void skipTestsForARM()
 43 |     {
 44 |         Assumptions.assumeFalse( System.getProperty( "os.arch" ).equals( "aarch64" ),
 45 |                                  "This test is ignored on ARM architecture, because Docker Compose Container doesn't support it." );
 46 |     }
 47 | 
 48 |     private ComposeContainer createContainer( File composeFile, Path containerRootDir, String serviceName )
 49 |     {
 50 |         var container = new ComposeContainer( composeFile );
 51 | 
 52 |         container.withExposedService( serviceName, DEFAULT_BOLT_PORT )
 53 |                  .withExposedService( serviceName, DEFAULT_HTTP_PORT )
 54 |                  .withEnv( "NEO4J_IMAGE", TestSettings.IMAGE_ID.asCanonicalNameString() )
 55 |                  .withEnv( "HOST_ROOT", containerRootDir.toAbsolutePath().toString() )
 56 |                  .waitingFor( serviceName, waitForBoltReady() )
 57 |                  .withLogConsumer( serviceName, new Slf4jLogConsumer( log ) );
 58 | 
 59 |         return container;
 60 |     }
 61 | 
 62 |     /* We need to stop the neo4j service before we stop the docker compose container otherwise there is a race condition for
 63 |        files that are written in mounted folders. This should not be needed when https://github.com/testcontainers/testcontainers-java/issues/9870 is fixed
 64 |     */
 65 |     private void stopContainerSafely( ComposeContainer container, String serviceName ) throws IOException
 66 |     {
 67 |         var containerId = container.getContainerByServiceName( serviceName ).get().getContainerId();
 68 | 
 69 |         DockerClient dockerClient = DockerClientFactory.lazyClient();
 70 |         dockerClient.stopContainerCmd( containerId ).exec();
 71 | 
 72 |         container.stop();
 73 |     }
 74 | 
 75 |     @Test
 76 |     void shouldCreateContainerAndConnect() throws Exception
 77 |     {
 78 |         var tmpDir = temporaryFolderManager.createFolder( "Simple_Container_Compose" );
 79 |         var composeFile = copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "simple-container-compose.yml" ).toFile() );
 80 |         var serviceName = "simplecontainer";
 81 | 
 82 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
 83 |         {
 84 |             dockerComposeContainer.start();
 85 | 
 86 |             var dbio = new DatabaseIO( dockerComposeContainer.getServiceHost( serviceName, DEFAULT_BOLT_PORT ),
 87 |                                        dockerComposeContainer.getServicePort( serviceName, DEFAULT_BOLT_PORT ) );
 88 |             dbio.verifyConnectivity( "neo4j", "simplecontainerpassword" );
 89 |             stopContainerSafely( dockerComposeContainer, serviceName );
 90 |         }
 91 |     }
 92 | 
 93 |     @Test
 94 |     void shouldCreateContainerWithSecretPasswordAndConnect() throws Exception
 95 |     {
 96 |         var tmpDir = temporaryFolderManager.createFolder( "Container_Compose_With_Secrets" );
 97 |         var composeFile = copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "container-compose-with-secrets.yml" ).toFile() );
 98 |         var serviceName = "secretscontainer";
 99 | 
100 |         var newSecretPassword = "neo4j/newSecretPassword";
101 |         Files.createFile( tmpDir.resolve( "neo4j_auth.txt" ) );
102 |         Files.writeString( tmpDir.resolve( "neo4j_auth.txt" ), newSecretPassword );
103 | 
104 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
105 |         {
106 |             dockerComposeContainer.start();
107 |             var dbio = new DatabaseIO( dockerComposeContainer.getServiceHost( serviceName, DEFAULT_BOLT_PORT ),
108 |                                        dockerComposeContainer.getServicePort( serviceName, DEFAULT_BOLT_PORT ) );
109 |             dbio.verifyConnectivity( "neo4j", "newSecretPassword" );
110 |             stopContainerSafely( dockerComposeContainer, serviceName );
111 |         }
112 |     }
113 | 
114 |     @Test
115 |     void shouldOverrideVariableWithSecretValue() throws Exception
116 |     {
117 |         var tmpDir = temporaryFolderManager.createFolder( "Container_Compose_With_Secrets_Override" );
118 |         Files.createDirectories( tmpDir.resolve( "neo4j" ).resolve( "config" ) );
119 | 
120 |         var composeFile = copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "container-compose-with-secrets-override.yml" ).toFile() );
121 |         var serviceName = "secretsoverridecontainer";
122 | 
123 |         var newSecretPageCache = "50M";
124 |         Files.createFile( tmpDir.resolve( "neo4j_pagecache.txt" ) );
125 |         Files.writeString( tmpDir.resolve( "neo4j_pagecache.txt" ), newSecretPageCache );
126 | 
127 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
128 |         {
129 |             dockerComposeContainer.start();
130 | 
131 |             var dbio = new DatabaseIO( dockerComposeContainer.getServiceHost( serviceName, DEFAULT_BOLT_PORT ),
132 |                                        dockerComposeContainer.getServicePort( serviceName, DEFAULT_BOLT_PORT ) );
133 | 
134 |             var secretSetting = dbio.getConfigurationSettingAsString( "neo4j",
135 |                                                                       "secretsoverridecontainerpassword",
136 |                                                                       Configuration.getConfigurationNameMap().get( Setting.MEMORY_PAGECACHE_SIZE ) );
137 | 
138 |             Assertions.assertTrue( secretSetting.contains( "50" ) );
139 | 
140 |             stopContainerSafely( dockerComposeContainer, serviceName );
141 |         }
142 |     }
143 | 
144 |     @Test
145 |     void shouldFailIfSecretFileDoesNotExist() throws Exception
146 |     {
147 |         var tmpDir = temporaryFolderManager.createFolder( "Container_Compose_With_Secrets_Override" );
148 |         var composeFile = copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "container-compose-with-secrets-override.yml" ).toFile() );
149 |         var serviceName = "secretsoverridecontainer";
150 | 
151 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
152 |         {
153 |             Assertions.assertThrows( Exception.class, dockerComposeContainer::start );
154 |         }
155 |     }
156 | 
157 |     @Test
158 |     void shouldFailAndPrintMessageIfFileIsNotReadable() throws Exception
159 |     {
160 |         var tmpDir = temporaryFolderManager.createFolder( "Container_Compose_With_Secrets_Override" );
161 |         var composeFile = copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "container-compose-with-secrets-override.yml" ).toFile() );
162 |         var serviceName = "secretsoverridecontainer";
163 | 
164 |         Files.createFile( tmpDir.resolve( "neo4j_pagecache.txt" ) );
165 |         Files.writeString( tmpDir.resolve( "neo4j_pagecache.txt" ), "50M" );
166 | 
167 |         var newPermissions = PosixFilePermissions.fromString( "rw-------" );
168 |         Files.setPosixFilePermissions( tmpDir.resolve( "neo4j_pagecache.txt" ), newPermissions );
169 | 
170 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
171 |         {
172 |             var containerLogConsumer = new ToStringConsumer();
173 |             dockerComposeContainer.withLogConsumer( serviceName, containerLogConsumer );
174 |             var expectedLogLine = "The secret file '/run/secrets/neo4j_dbms_memory_pagecache_size_file' does not exist or is not readable. " +
175 |                                   "Make sure you have correctly configured docker secrets.";
176 |             Assertions.assertThrows( Exception.class, dockerComposeContainer::start );
177 |             Assertions.assertTrue( containerLogConsumer.toUtf8String().contains( expectedLogLine ) );
178 |         }
179 |     }
180 | 
181 |     @Test
182 |     void shouldIgnoreNonNeo4jFileEnvVars() throws Exception
183 |     {
184 |         var tmpDir = temporaryFolderManager.createFolder( "Simple_Container_Compose_With_File_Var" );
185 |         var composeFile =
186 |                 copyDockerComposeResourceFile( tmpDir, TEST_RESOURCES_PATH.resolve( "simple-container-compose-with-external-file-var.yml" ).toFile() );
187 |         var serviceName = "simplecontainer";
188 | 
189 |         try ( var dockerComposeContainer = createContainer( composeFile, tmpDir, serviceName ) )
190 |         {
191 |             dockerComposeContainer.start();
192 | 
193 |             var dbio = new DatabaseIO( dockerComposeContainer.getServiceHost( serviceName, DEFAULT_BOLT_PORT ),
194 |                                        dockerComposeContainer.getServicePort( serviceName, DEFAULT_BOLT_PORT ) );
195 |             dbio.verifyConnectivity( "neo4j", "simplecontainerpassword" );
196 | 
197 |             stopContainerSafely( dockerComposeContainer, serviceName );
198 |         }
199 |     }
200 | 
201 |     private File copyDockerComposeResourceFile( Path targetDirectory, File resourceFile ) throws IOException
202 |     {
203 |         File compose_file = new File( targetDirectory.toString(), resourceFile.getName() );
204 |         if ( compose_file.exists() )
205 |         {
206 |             Files.delete( compose_file.toPath() );
207 |         }
208 |         Files.copy( resourceFile.toPath(), Paths.get( compose_file.getPath() ) );
209 |         return compose_file;
210 |     }
211 | }
212 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestUpgrade.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb;
  2 | 
  3 | import com.github.dockerjava.api.command.CreateContainerCmd;
  4 | import com.github.dockerjava.api.exception.NotFoundException;
  5 | import com.github.dockerjava.api.model.Bind;
  6 | import com.neo4j.docker.utils.DatabaseIO;
  7 | import com.neo4j.docker.utils.Neo4jVersion;
  8 | import com.neo4j.docker.utils.TemporaryFolderManager;
  9 | import com.neo4j.docker.utils.TestSettings;
 10 | import org.junit.jupiter.api.Assumptions;
 11 | import org.junit.jupiter.api.extension.RegisterExtension;
 12 | import org.junit.jupiter.params.ParameterizedTest;
 13 | import org.junit.jupiter.params.provider.MethodSource;
 14 | import org.slf4j.Logger;
 15 | import org.slf4j.LoggerFactory;
 16 | import org.testcontainers.containers.GenericContainer;
 17 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 18 | import org.testcontainers.images.RemoteDockerImage;
 19 | import org.testcontainers.utility.DockerImageName;
 20 | 
 21 | import java.io.IOException;
 22 | import java.nio.file.Path;
 23 | import java.util.Arrays;
 24 | import java.util.List;
 25 | import java.util.Random;
 26 | import java.util.function.Consumer;
 27 | 
 28 | import static org.junit.jupiter.api.Assumptions.assumeTrue;
 29 | 
 30 | public class TestUpgrade
 31 | {
 32 | 	private final Logger log = LoggerFactory.getLogger( TestUpgrade.class );
 33 | 	private final String user = "neo4j";
 34 | 	private final String password = "verylongpassword";
 35 |     @RegisterExtension
 36 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 37 | 
 38 | 	private GenericContainer makeContainer(DockerImageName image)
 39 | 	{
 40 |         GenericContainer container = new GenericContainer<>( image );
 41 |         container.withEnv( "NEO4J_AUTH", user + "/" + password )
 42 |                  .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 43 |                  .withExposedPorts( 7474 )
 44 |                  .withExposedPorts( 7687 )
 45 |                  .withLogConsumer( new Slf4jLogConsumer( log ) );
 46 |         return container;
 47 | 	}
 48 | 
 49 | 	private static List<Neo4jVersion> upgradableNeo4jVersionsPre5()
 50 | 	{
 51 | 		return Arrays.asList( new Neo4jVersion( 3, 5, 35 ),
 52 | 							  new Neo4jVersion( 4, 4, 0 ),
 53 | 							  new Neo4jVersion( 4, 4, 25 ));
 54 |     }
 55 | 
 56 |     private static List<Neo4jVersion> upgradableNeo4jVersions5x()
 57 |     {
 58 |         // instead of returning ALL 5.x versions just run a few to check that upgrading works and the volume/bind mount
 59 |         // settings do not break upgrade.
 60 |         // Running every upgrade path used up all the test agent memorry and caused unneccessary failures.
 61 |         // We must assume that Neo4j upgrades are fully tested elsewhere, and just make sure that the
 62 |         // docker infrastructure doesn't break upgrading.
 63 | 		return Arrays.asList( new Neo4jVersion( 5, 1, 0 ),
 64 |                               new Neo4jVersion( 5, 5, 0 ),
 65 | 							  new Neo4jVersion( 5, 10, 0 ));
 66 |     }
 67 | 
 68 |     private static List<Neo4jVersion> upgradableNeo4jVersionsCalVer()
 69 |     {
 70 | 		return Arrays.asList( new Neo4jVersion( 5, 26, 0 ));
 71 |     }
 72 | 
 73 |     private static void assumeUpgradeSupported( Neo4jVersion upgradeFrom )
 74 |     {
 75 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isNewerThan( upgradeFrom ),
 76 |                     "cannot upgrade from " + upgradeFrom + " to " + TestSettings.NEO4J_VERSION);
 77 |         if(isArm()) Assumptions.assumeTrue( upgradeFrom.isAtLeastVersion( new Neo4jVersion( 4, 4, 0 ) ), "ARM only supported since 4.4" );
 78 | 
 79 |         // if we're preparing a new release, then it's possible the version we're upgrading from hasn't been released to
 80 |         // dockerhub, so the test will fail when pulling the upgrade-from image.
 81 |         // If this happens we should ignore rather than fail the test.
 82 |         try
 83 |         {
 84 |             RemoteDockerImage img = new RemoteDockerImage( getUpgradeFromImage( upgradeFrom ) );
 85 |             img.get(); // docker pull
 86 |         }
 87 |         catch ( NotFoundException nfex )
 88 |         {
 89 |             // purposely fail an assumption if the image was not found
 90 |             Assumptions.assumeTrue( false, "neo4j:"+upgradeFrom+" is not available on dockerhub yet. Ignoring test.");
 91 |         }
 92 |     }
 93 | 
 94 |     private static boolean isArm()
 95 |     {
 96 |         return System.getProperty( "os.arch" ).equals( "aarch64" );
 97 |     }
 98 | 
 99 | 	@ParameterizedTest(name = "from_{0}")
100 |     @MethodSource( "upgradableNeo4jVersionsPre5" )
101 | 	void canUpgradeNeo4j_fileMounts_Pre5( Neo4jVersion upgradeFrom) throws Exception
102 | 	{
103 | 		assumeTrue( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
104 |                     "this test only for upgrades before 5.0: " + TestSettings.NEO4J_VERSION );
105 | 		testUpgradeFileMounts( upgradeFrom );
106 | 	}
107 | 
108 | 	@ParameterizedTest(name = "from_{0}")
109 | 	@MethodSource( "upgradableNeo4jVersionsPre5" )
110 | 	void canUpgradeNeo4j_namedVolumes_Pre5(Neo4jVersion upgradeFrom) throws Exception
111 | 	{
112 | 		assumeTrue( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
113 |                     "this test only for upgrades before 5.0: " + TestSettings.NEO4J_VERSION );
114 | 		testUpgradeNamedVolumes( upgradeFrom );
115 | 	}
116 | 
117 | 	@ParameterizedTest(name = "from_{0}")
118 |     @MethodSource( "upgradableNeo4jVersions5x" )
119 | 	void canUpgradeNeo4j_fileMounts_5x( Neo4jVersion upgradeFrom) throws Exception
120 | 	{
121 | 		assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ),
122 |                     "this test only for upgrades after 5.0: " + TestSettings.NEO4J_VERSION );
123 | 		assumeTrue( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_527 ),
124 |                     "this test only for upgrades on the 5x branch" + TestSettings.NEO4J_VERSION );
125 | 		testUpgradeFileMounts( upgradeFrom );
126 | 	}
127 | 
128 | 	@ParameterizedTest(name = "from_{0}")
129 | 	@MethodSource( "upgradableNeo4jVersions5x" )
130 | 	void canUpgradeNeo4j_namedVolumes_5x(Neo4jVersion upgradeFrom) throws Exception
131 | 	{
132 | 		assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ),
133 |                     "this test only for upgrades after 5.0: " + TestSettings.NEO4J_VERSION );
134 | 		assumeTrue( TestSettings.NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_527 ),
135 |                     "this test only for upgrades on the 5x branch" + TestSettings.NEO4J_VERSION );
136 | 		testUpgradeNamedVolumes( upgradeFrom );
137 | 	}
138 | 
139 | 	@ParameterizedTest(name = "from_{0}")
140 |     @MethodSource( "upgradableNeo4jVersionsCalVer" )
141 | 	void canUpgradeNeo4j_fileMounts_calver( Neo4jVersion upgradeFrom) throws Exception
142 | 	{
143 | 		assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_527 ),
144 |                     "this test only for upgrades after 5.0: " + TestSettings.NEO4J_VERSION );
145 | 		testUpgradeFileMounts( upgradeFrom );
146 | 	}
147 | 
148 | 	@ParameterizedTest(name = "from_{0}")
149 | 	@MethodSource( "upgradableNeo4jVersionsCalVer" )
150 | 	void canUpgradeNeo4j_namedVolumes_calver(Neo4jVersion upgradeFrom) throws Exception
151 | 	{
152 | 		assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_527 ),
153 |                     "this test only for upgrades after 5.0: " + TestSettings.NEO4J_VERSION );
154 | 		testUpgradeNamedVolumes( upgradeFrom );
155 | 	}
156 | 
157 | 	private void testUpgradeFileMounts( Neo4jVersion upgradeFrom ) throws IOException
158 | 	{
159 | 		assumeUpgradeSupported( upgradeFrom );
160 | 
161 | 		Path data, logs, imports, metrics;
162 | 
163 | 		try(GenericContainer container = makeContainer( getUpgradeFromImage( upgradeFrom ) ))
164 | 		{
165 | 			data = temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
166 | 			logs = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
167 | 			imports = temporaryFolderManager.createFolderAndMountAsVolume(container, "/import");
168 | 			metrics = temporaryFolderManager.createFolderAndMountAsVolume(container, "/metrics");
169 | 			container.start();
170 | 			DatabaseIO db = new DatabaseIO( container );
171 | 			db.putInitialDataIntoContainer( user, password );
172 | 			// stops container cleanly so that neo4j process has enough time to end. The autoclose doesn't seem to block.
173 | 			container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec();
174 | 		}
175 | 
176 | 		try(GenericContainer container = makeContainer( TestSettings.IMAGE_ID ))
177 | 		{
178 | 			temporaryFolderManager.mountHostFolderAsVolume( container, data, "/data" );
179 | 			temporaryFolderManager.mountHostFolderAsVolume( container, logs, "/logs" );
180 | 			temporaryFolderManager.mountHostFolderAsVolume( container, imports, "/import" );
181 | 			temporaryFolderManager.mountHostFolderAsVolume( container, metrics, "/metrics" );
182 | 			container.withEnv( "NEO4J_dbms_allow__upgrade", "true" );
183 | 			container.start();
184 | 			DatabaseIO db = new DatabaseIO( container );
185 | 			db.verifyInitialDataInContainer( user, password );
186 | 		}
187 | 	}
188 | 
189 | 	private void testUpgradeNamedVolumes( Neo4jVersion upgradeFrom )
190 | 	{
191 | 		assumeUpgradeSupported(upgradeFrom);
192 | 
193 | 		String id = String.format( "%04d", new Random().nextInt( 10000 ));
194 | 		log.info( "creating volumes with id: "+id );
195 | 
196 | 		try(GenericContainer container = makeContainer(getUpgradeFromImage( upgradeFrom )))
197 | 		{
198 | 			container.withCreateContainerCmdModifier(
199 | 					(Consumer<CreateContainerCmd>) cmd -> cmd.getHostConfig().withBinds(
200 | 							Bind.parse("upgrade-conf-"+id+":/conf"),
201 | 							Bind.parse("upgrade-data-"+id+":/data"),
202 | 							Bind.parse("upgrade-import-"+id+":/import"),
203 | 							Bind.parse("upgrade-logs-"+id+":/logs"),
204 | 							Bind.parse("upgrade-metrics-"+id+":/metrics"),
205 | 							Bind.parse("upgrade-plugins-"+id+":/plugins")
206 | 					));
207 | 			container.start();
208 | 			DatabaseIO db = new DatabaseIO( container );
209 | 			db.putInitialDataIntoContainer( user, password );
210 | 			container.getDockerClient().stopContainerCmd( container.getContainerId() ).exec();
211 | 		}
212 | 
213 | 		try(GenericContainer container = makeContainer( TestSettings.IMAGE_ID ))
214 | 		{
215 | 			container.withCreateContainerCmdModifier(
216 | 					(Consumer<CreateContainerCmd>) cmd -> cmd.getHostConfig().withBinds(
217 | 							Bind.parse("upgrade-conf-"+id+":/conf"),
218 | 							Bind.parse("upgrade-data-"+id+":/data"),
219 | 							Bind.parse("upgrade-import-"+id+":/import"),
220 | 							Bind.parse("upgrade-logs-"+id+":/logs"),
221 | 							Bind.parse("upgrade-metrics-"+id+":/metrics"),
222 | 							Bind.parse("upgrade-plugins-"+id+":/plugins")
223 | 					));
224 | 			container.withEnv( "NEO4J_dbms_allow__upgrade", "true" );
225 | 			container.start();
226 | 			DatabaseIO db = new DatabaseIO( container );
227 | 			db.verifyInitialDataInContainer( user, password );
228 | 		}
229 | 	}
230 | 
231 | 	private static DockerImageName getUpgradeFromImage( Neo4jVersion ver)
232 | 	{
233 | 		if(TestSettings.EDITION == TestSettings.Edition.ENTERPRISE)
234 | 		{
235 | 			return DockerImageName.parse("neo4j:" + ver.toString() + "-enterprise");
236 | 		}
237 | 		else
238 | 		{
239 | 			return DockerImageName.parse("neo4j:" + ver.toString());
240 | 		}
241 | 	}
242 | }
243 | 
```

--------------------------------------------------------------------------------
/knowledge_graphs/ai_hallucination_detector.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | AI Hallucination Detector
  3 | 
  4 | Main orchestrator for detecting AI coding assistant hallucinations in Python scripts.
  5 | Combines AST analysis, knowledge graph validation, and comprehensive reporting.
  6 | """
  7 | 
  8 | import asyncio
  9 | import argparse
 10 | import logging
 11 | import os
 12 | import sys
 13 | from pathlib import Path
 14 | from typing import Optional, List
 15 | 
 16 | from dotenv import load_dotenv
 17 | 
 18 | from ai_script_analyzer import AIScriptAnalyzer, analyze_ai_script
 19 | from knowledge_graph_validator import KnowledgeGraphValidator
 20 | from hallucination_reporter import HallucinationReporter
 21 | 
 22 | # Configure logging
 23 | logging.basicConfig(
 24 |     level=logging.INFO,
 25 |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
 26 |     datefmt='%Y-%m-%d %H:%M:%S'
 27 | )
 28 | logger = logging.getLogger(__name__)
 29 | 
 30 | 
 31 | class AIHallucinationDetector:
 32 |     """Main detector class that orchestrates the entire process"""
 33 |     
 34 |     def __init__(self, neo4j_uri: str, neo4j_user: str, neo4j_password: str):
 35 |         self.validator = KnowledgeGraphValidator(neo4j_uri, neo4j_user, neo4j_password)
 36 |         self.reporter = HallucinationReporter()
 37 |         self.analyzer = AIScriptAnalyzer()
 38 |     
 39 |     async def initialize(self):
 40 |         """Initialize connections and components"""
 41 |         await self.validator.initialize()
 42 |         logger.info("AI Hallucination Detector initialized successfully")
 43 |     
 44 |     async def close(self):
 45 |         """Close connections"""
 46 |         await self.validator.close()
 47 |     
 48 |     async def detect_hallucinations(self, script_path: str, 
 49 |                                   output_dir: Optional[str] = None,
 50 |                                   save_json: bool = True,
 51 |                                   save_markdown: bool = True,
 52 |                                   print_summary: bool = True) -> dict:
 53 |         """
 54 |         Main detection function that analyzes a script and generates reports
 55 |         
 56 |         Args:
 57 |             script_path: Path to the AI-generated Python script
 58 |             output_dir: Directory to save reports (defaults to script directory)
 59 |             save_json: Whether to save JSON report
 60 |             save_markdown: Whether to save Markdown report
 61 |             print_summary: Whether to print summary to console
 62 |         
 63 |         Returns:
 64 |             Complete validation report as dictionary
 65 |         """
 66 |         logger.info(f"Starting hallucination detection for: {script_path}")
 67 |         
 68 |         # Validate input
 69 |         if not os.path.exists(script_path):
 70 |             raise FileNotFoundError(f"Script not found: {script_path}")
 71 |         
 72 |         if not script_path.endswith('.py'):
 73 |             raise ValueError("Only Python (.py) files are supported")
 74 |         
 75 |         # Set output directory
 76 |         if output_dir is None:
 77 |             output_dir = str(Path(script_path).parent)
 78 |         
 79 |         os.makedirs(output_dir, exist_ok=True)
 80 |         
 81 |         try:
 82 |             # Step 1: Analyze the script using AST
 83 |             logger.info("Step 1: Analyzing script structure...")
 84 |             analysis_result = self.analyzer.analyze_script(script_path)
 85 |             
 86 |             if analysis_result.errors:
 87 |                 logger.warning(f"Analysis warnings: {analysis_result.errors}")
 88 |             
 89 |             logger.info(f"Found: {len(analysis_result.imports)} imports, "
 90 |                        f"{len(analysis_result.class_instantiations)} class instantiations, "
 91 |                        f"{len(analysis_result.method_calls)} method calls, "
 92 |                        f"{len(analysis_result.function_calls)} function calls, "
 93 |                        f"{len(analysis_result.attribute_accesses)} attribute accesses")
 94 |             
 95 |             # Step 2: Validate against knowledge graph
 96 |             logger.info("Step 2: Validating against knowledge graph...")
 97 |             validation_result = await self.validator.validate_script(analysis_result)
 98 |             
 99 |             logger.info(f"Validation complete. Overall confidence: {validation_result.overall_confidence:.1%}")
100 |             
101 |             # Step 3: Generate comprehensive report
102 |             logger.info("Step 3: Generating reports...")
103 |             report = self.reporter.generate_comprehensive_report(validation_result)
104 |             
105 |             # Step 4: Save reports
106 |             script_name = Path(script_path).stem
107 |             
108 |             if save_json:
109 |                 json_path = os.path.join(output_dir, f"{script_name}_hallucination_report.json")
110 |                 self.reporter.save_json_report(report, json_path)
111 |             
112 |             if save_markdown:
113 |                 md_path = os.path.join(output_dir, f"{script_name}_hallucination_report.md")
114 |                 self.reporter.save_markdown_report(report, md_path)
115 |             
116 |             # Step 5: Print summary
117 |             if print_summary:
118 |                 self.reporter.print_summary(report)
119 |             
120 |             logger.info("Hallucination detection completed successfully")
121 |             return report
122 |             
123 |         except Exception as e:
124 |             logger.error(f"Error during hallucination detection: {str(e)}")
125 |             raise
126 |     
127 |     async def batch_detect(self, script_paths: List[str], 
128 |                           output_dir: Optional[str] = None) -> List[dict]:
129 |         """
130 |         Detect hallucinations in multiple scripts
131 |         
132 |         Args:
133 |             script_paths: List of paths to Python scripts
134 |             output_dir: Directory to save all reports
135 |         
136 |         Returns:
137 |             List of validation reports
138 |         """
139 |         logger.info(f"Starting batch detection for {len(script_paths)} scripts")
140 |         
141 |         results = []
142 |         for i, script_path in enumerate(script_paths, 1):
143 |             logger.info(f"Processing script {i}/{len(script_paths)}: {script_path}")
144 |             
145 |             try:
146 |                 result = await self.detect_hallucinations(
147 |                     script_path=script_path,
148 |                     output_dir=output_dir,
149 |                     print_summary=False  # Don't print individual summaries in batch mode
150 |                 )
151 |                 results.append(result)
152 |                 
153 |             except Exception as e:
154 |                 logger.error(f"Failed to process {script_path}: {str(e)}")
155 |                 # Continue with other scripts
156 |                 continue
157 |         
158 |         # Print batch summary
159 |         self._print_batch_summary(results)
160 |         
161 |         return results
162 |     
163 |     def _print_batch_summary(self, results: List[dict]):
164 |         """Print summary of batch processing results"""
165 |         if not results:
166 |             print("No scripts were successfully processed.")
167 |             return
168 |         
169 |         print("\n" + "="*80)
170 |         print("🚀 BATCH HALLUCINATION DETECTION SUMMARY")
171 |         print("="*80)
172 |         
173 |         total_scripts = len(results)
174 |         total_validations = sum(r['validation_summary']['total_validations'] for r in results)
175 |         total_valid = sum(r['validation_summary']['valid_count'] for r in results)
176 |         total_invalid = sum(r['validation_summary']['invalid_count'] for r in results)
177 |         total_not_found = sum(r['validation_summary']['not_found_count'] for r in results)
178 |         total_hallucinations = sum(len(r['hallucinations_detected']) for r in results)
179 |         
180 |         avg_confidence = sum(r['validation_summary']['overall_confidence'] for r in results) / total_scripts
181 |         
182 |         print(f"Scripts Processed: {total_scripts}")
183 |         print(f"Total Validations: {total_validations}")
184 |         print(f"Average Confidence: {avg_confidence:.1%}")
185 |         print(f"Total Hallucinations: {total_hallucinations}")
186 |         
187 |         print(f"\nAggregated Results:")
188 |         print(f"  ✅ Valid: {total_valid} ({total_valid/total_validations:.1%})")
189 |         print(f"  ❌ Invalid: {total_invalid} ({total_invalid/total_validations:.1%})")
190 |         print(f"  🔍 Not Found: {total_not_found} ({total_not_found/total_validations:.1%})")
191 |         
192 |         # Show worst performing scripts
193 |         print(f"\n🚨 Scripts with Most Hallucinations:")
194 |         sorted_results = sorted(results, key=lambda x: len(x['hallucinations_detected']), reverse=True)
195 |         for result in sorted_results[:5]:
196 |             script_name = Path(result['analysis_metadata']['script_path']).name
197 |             hall_count = len(result['hallucinations_detected'])
198 |             confidence = result['validation_summary']['overall_confidence']
199 |             print(f"  - {script_name}: {hall_count} hallucinations ({confidence:.1%} confidence)")
200 |         
201 |         print("="*80)
202 | 
203 | 
204 | async def main():
205 |     """Command-line interface for the AI Hallucination Detector"""
206 |     parser = argparse.ArgumentParser(
207 |         description="Detect AI coding assistant hallucinations in Python scripts",
208 |         formatter_class=argparse.RawDescriptionHelpFormatter,
209 |         epilog="""
210 | Examples:
211 |   # Analyze single script
212 |   python ai_hallucination_detector.py script.py
213 |   
214 |   # Analyze multiple scripts
215 |   python ai_hallucination_detector.py script1.py script2.py script3.py
216 |   
217 |   # Specify output directory
218 |   python ai_hallucination_detector.py script.py --output-dir reports/
219 |   
220 |   # Skip markdown report
221 |   python ai_hallucination_detector.py script.py --no-markdown
222 |         """
223 |     )
224 |     
225 |     parser.add_argument(
226 |         'scripts',
227 |         nargs='+',
228 |         help='Python script(s) to analyze for hallucinations'
229 |     )
230 |     
231 |     parser.add_argument(
232 |         '--output-dir',
233 |         help='Directory to save reports (defaults to script directory)'
234 |     )
235 |     
236 |     parser.add_argument(
237 |         '--no-json',
238 |         action='store_true',
239 |         help='Skip JSON report generation'
240 |     )
241 |     
242 |     parser.add_argument(
243 |         '--no-markdown',
244 |         action='store_true',
245 |         help='Skip Markdown report generation'
246 |     )
247 |     
248 |     parser.add_argument(
249 |         '--no-summary',
250 |         action='store_true',
251 |         help='Skip printing summary to console'
252 |     )
253 |     
254 |     parser.add_argument(
255 |         '--neo4j-uri',
256 |         default=None,
257 |         help='Neo4j URI (default: from environment NEO4J_URI)'
258 |     )
259 |     
260 |     parser.add_argument(
261 |         '--neo4j-user',
262 |         default=None,
263 |         help='Neo4j username (default: from environment NEO4J_USER)'
264 |     )
265 |     
266 |     parser.add_argument(
267 |         '--neo4j-password',
268 |         default=None,
269 |         help='Neo4j password (default: from environment NEO4J_PASSWORD)'
270 |     )
271 |     
272 |     parser.add_argument(
273 |         '--verbose',
274 |         action='store_true',
275 |         help='Enable verbose logging'
276 |     )
277 |     
278 |     args = parser.parse_args()
279 |     
280 |     if args.verbose:
281 |         logging.getLogger().setLevel(logging.INFO)
282 |         # Only enable debug for our modules, not neo4j
283 |         logging.getLogger('neo4j').setLevel(logging.WARNING)
284 |         logging.getLogger('neo4j.pool').setLevel(logging.WARNING)
285 |         logging.getLogger('neo4j.io').setLevel(logging.WARNING)
286 |     
287 |     # Load environment variables
288 |     load_dotenv()
289 |     
290 |     # Get Neo4j credentials
291 |     neo4j_uri = args.neo4j_uri or os.environ.get('NEO4J_URI', 'bolt://localhost:7687')
292 |     neo4j_user = args.neo4j_user or os.environ.get('NEO4J_USER', 'neo4j')
293 |     neo4j_password = args.neo4j_password or os.environ.get('NEO4J_PASSWORD', 'password')
294 |     
295 |     if not neo4j_password or neo4j_password == 'password':
296 |         logger.error("Please set NEO4J_PASSWORD environment variable or use --neo4j-password")
297 |         sys.exit(1)
298 |     
299 |     # Initialize detector
300 |     detector = AIHallucinationDetector(neo4j_uri, neo4j_user, neo4j_password)
301 |     
302 |     try:
303 |         await detector.initialize()
304 |         
305 |         # Process scripts
306 |         if len(args.scripts) == 1:
307 |             # Single script mode
308 |             await detector.detect_hallucinations(
309 |                 script_path=args.scripts[0],
310 |                 output_dir=args.output_dir,
311 |                 save_json=not args.no_json,
312 |                 save_markdown=not args.no_markdown,
313 |                 print_summary=not args.no_summary
314 |             )
315 |         else:
316 |             # Batch mode
317 |             await detector.batch_detect(
318 |                 script_paths=args.scripts,
319 |                 output_dir=args.output_dir
320 |             )
321 |     
322 |     except KeyboardInterrupt:
323 |         logger.info("Detection interrupted by user")
324 |         sys.exit(1)
325 |     
326 |     except Exception as e:
327 |         logger.error(f"Detection failed: {str(e)}")
328 |         sys.exit(1)
329 |     
330 |     finally:
331 |         await detector.close()
332 | 
333 | 
334 | if __name__ == "__main__":
335 |     asyncio.run(main())
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/utils/TemporaryFolderManager.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.utils;
  2 | 
  3 | import org.apache.commons.compress.archivers.ArchiveEntry;
  4 | import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
  5 | import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
  6 | import org.apache.commons.io.FileUtils;
  7 | import org.junit.jupiter.api.extension.AfterAllCallback;
  8 | import org.junit.jupiter.api.extension.BeforeEachCallback;
  9 | import org.junit.jupiter.api.extension.ExtensionContext;
 10 | import org.slf4j.Logger;
 11 | import org.slf4j.LoggerFactory;
 12 | import org.testcontainers.containers.BindMode;
 13 | import org.testcontainers.containers.Container;
 14 | import org.testcontainers.containers.GenericContainer;
 15 | import org.testcontainers.containers.wait.strategy.Wait;
 16 | import org.testcontainers.shaded.org.apache.commons.io.IOUtils;
 17 | import org.testcontainers.utility.DockerImageName;
 18 | 
 19 | import java.io.IOException;
 20 | import java.io.InputStream;
 21 | import java.io.OutputStream;
 22 | import java.nio.file.Files;
 23 | import java.nio.file.Path;
 24 | import java.time.Duration;
 25 | import java.util.HashSet;
 26 | import java.util.List;
 27 | import java.util.Random;
 28 | import java.util.Set;
 29 | import java.util.stream.Collectors;
 30 | 
 31 | /**JUnit extension to create temporary folders and compress them after each test class runs.
 32 |  * <p>
 33 |  * <h2>WHY</h2>
 34 |  * Starting a clean neo4j pre-allocates 500MB of space for the data folder. With all these docker tests
 35 |  * this ends up allocating a huge amount of empty space that fills the test machine memory.
 36 |  * There are enough unit tests now, that we frequently get test failures just because
 37 |  * the machine running the tests ran out of space.
 38 |  * This empty space can easily be freed by compressing the mounted folders once we are finished with them.
 39 |  * <p>
 40 |  *
 41 |  * <h2>HOW</h2>
 42 |  * To use this utility, create an object as a class field, and use @RegisterExtension annotation.
 43 |  *
 44 |  * On using TemporaryFolderManager to create a folder for the  first time in a test method, it will create a folder
 45 |  * with the method name (plus a little salt), and put all the method's temporary folders and data inside.
 46 |  * This means...
 47 |  * <ul>
 48 |  *     <li>when creating temporary folders, you don't need to worry about using the same folder name as another test.</li>
 49 |  *     <li>you can give folders generic names without having to worry too much about them being descriptive.</li>
 50 |  *     <li>Folder names can be automatically generated from the mountpoint if desired</li>
 51 |  * </ul>
 52 |  *
 53 |  * <h2>EXAMPLE USE CASES</h2>
 54 |  *
 55 |  * First, to get a TemporaryFolderManager instantiation do:
 56 |  * <pre>{@code
 57 |  @RegisterExtension
 58 |  public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 59 |  * }</pre>
 60 |  *
 61 |  * <h3>SIMPLE: Just create one or two unrelated folders and mount them</h3>
 62 |  * Most of the time, this is all you'll need.
 63 |  * Assuming you already have a container...
 64 |  * <pre>{@code
 65 | Path confpath = temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
 66 | Path logpath = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
 67 |  * }</pre>
 68 |  * This will create a folder in {@link TestSettings#TEST_TMP_FOLDER} with the name
 69 |  * <code>CLASSNAME_METHODNAME_RANDOMNUMBER</code> and inside it, there will be a folder called <code>conf</code> and
 70 |  * a folder called <code>logs</code>. These will be mounted to <code>container</code> at <code>/conf</code> and <code>/logs</code>.
 71 |  * <p>
 72 |  * For example if your test class is TestMounting.java and the method is called <code>shouldWriteToMount</code>
 73 |  * the folders created will be together inside <code>com.neo4j.docker.coredb.TestMounting_shouldWriteToMount_RANDOMNUMBER</code>.
 74 |  *
 75 |  * <h3>HARDER: Mount the same folder to two different (consecutive) containers</h3>
 76 |  * <pre>{@code
 77 | Path confpath;
 78 | try(container1 = makeAContainer())
 79 | {
 80 |     confpath = temporaryFolderManager.createFolderAndMountAsVolume(container1, "/conf");
 81 | }
 82 | try(container2 = makeAContainer())
 83 | {
 84 |     temporaryFolderManager.mountHostFolderAsVolume(container2, confpath, "/conf");
 85 | }
 86 |  * }</pre>
 87 |  *
 88 |  * <h3>HARDEST: Two containers, two different folders, same mount point each time</h3>
 89 |  * <pre>{@code
 90 | try(container1 = makeAContainer())
 91 | {
 92 |     Path confpath1 = temporaryFolderManager.createNamedFolderAndMountAsVolume( container1, "conf1", "/conf" );
 93 | }
 94 | try(container2 = makeAContainer())
 95 | {
 96 |     Path confpath2 = temporaryFolderManager.createNamedFolderAndMountAsVolume( container2, "conf2", "/conf" );
 97 | }
 98 |  * }</pre>
 99 |  * */
100 | public class TemporaryFolderManager implements AfterAllCallback, BeforeEachCallback
101 | {
102 |     private final Logger log = LoggerFactory.getLogger( TemporaryFolderManager.class );
103 |     // if we ever run parallel tests, random number generator and
104 |     // list of folders to compress need to be made thread safe
105 |     private Random rng = new Random(  );
106 |     private final Path folderRoot;
107 |     protected Path methodOutputFolder;    // protected scope for testing
108 |     protected Set<Path> toCompressAfterAll = new HashSet<>();    // protected scope for testing
109 | 
110 |     public TemporaryFolderManager( )
111 |     {
112 |         this(TestSettings.TEST_TMP_FOLDER);
113 |     }
114 |     public TemporaryFolderManager( Path testOutputParentFolder )
115 |     {
116 |         this.folderRoot = testOutputParentFolder;
117 |     }
118 | 
119 |     @Override
120 |     public void beforeEach( ExtensionContext extensionContext ) throws Exception
121 |     {
122 |         String methodOutputFolderName = extensionContext.getTestClass().get().getName() + "_" +
123 |                                  extensionContext.getTestMethod().get().getName();
124 |         if(!extensionContext.getDisplayName().startsWith( extensionContext.getTestMethod().get().getName() ))
125 |         {
126 |             methodOutputFolderName += "_" + extensionContext.getDisplayName()
127 |                                                             .replace( ' ', '_' );
128 |         }
129 |         // finally add some salt so  that we can run the same test method twice and not get naming clashes.
130 |         methodOutputFolderName += String.format( "_%04d", rng.nextInt(10000 ) );
131 |         log.info( "Recommended folder prefix is " + methodOutputFolderName );
132 |         methodOutputFolder = folderRoot.resolve( methodOutputFolderName );
133 |     }
134 | 
135 |     @Override
136 |     public void afterAll( ExtensionContext extensionContext ) throws Exception
137 |     {
138 |         triggerCleanup();
139 |     }
140 | 
141 |     public void triggerCleanup() throws Exception
142 |     {
143 |         if(TestSettings.SKIP_MOUNTED_FOLDER_TARBALLING)
144 |         {
145 |             log.info( "Cleanup of test artifacts skipped by request" );
146 |             return;
147 |         }
148 |         log.info( "Performing cleanup of {}", folderRoot );
149 |         // create tar archive of data
150 |         for(Path p : toCompressAfterAll)
151 |         {
152 |             String tarOutName = p.getFileName().toString() + ".tar.gz";
153 |             try ( OutputStream fo = Files.newOutputStream( p.getParent().resolve( tarOutName ) );
154 |                   OutputStream gzo = new GzipCompressorOutputStream( fo );
155 |                   TarArchiveOutputStream archiver = new TarArchiveOutputStream( gzo ) )
156 |             {
157 |                 archiver.setLongFileMode( TarArchiveOutputStream.LONGFILE_POSIX );
158 |                 List<Path> files = Files.walk( p ).toList();
159 |                 for(Path fileToBeArchived : files)
160 |                 {
161 |                     // don't archive directories...
162 |                     if(fileToBeArchived.toFile().isDirectory()) continue;
163 |                     try( InputStream fileStream = Files.newInputStream( fileToBeArchived ))
164 |                     {
165 |                         ArchiveEntry entry = archiver.createArchiveEntry( fileToBeArchived, folderRoot.relativize( fileToBeArchived ).toString() );
166 |                         archiver.putArchiveEntry( entry );
167 |                         IOUtils.copy( fileStream, archiver );
168 |                         archiver.closeArchiveEntry();
169 |                     } catch (IOException ioe)
170 |                     {
171 |                         // consume the error, because sometimes, file permissions won't let us copy
172 |                         log.warn( "Could not archive "+ fileToBeArchived, ioe);
173 |                     }
174 |                 }
175 |                 archiver.finish();
176 |             }
177 |         }
178 |         // delete original folders
179 |         log.debug( "Re owning folders: {}", toCompressAfterAll.stream()
180 |                                                               .map( Path::toString )
181 |                                                               .collect( Collectors.joining(", ")));
182 |         setFolderOwnerTo( SetContainerUser.getNonRootUserString(),
183 |                           toCompressAfterAll.toArray(new Path[toCompressAfterAll.size()]) );
184 | 
185 |         for(Path p : toCompressAfterAll)
186 |         {
187 |             log.debug( "Deleting test output folder {}", p.getFileName().toString() );
188 |             FileUtils.deleteDirectory( p.toFile() );
189 |         }
190 |         toCompressAfterAll.clear();
191 |     }
192 | 
193 |     public Path createNamedFolderAndMountAsVolume( GenericContainer container, String hostFolderName, String containerMountPoint ) throws IOException
194 |     {
195 |         Path tempFolder = createFolder( hostFolderName );
196 |         mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
197 |         return tempFolder;
198 |     }
199 | 
200 |     public Path createFolderAndMountAsVolume( GenericContainer container, String containerMountPoint ) throws IOException
201 |     {
202 |         Path tempFolder = createFolder( getFolderNameFromMountPoint( containerMountPoint ) );
203 |         mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
204 |         return tempFolder;
205 |     }
206 | 
207 | //    public Path createNamedFolderAndMountAsVolume( GenericContainer container, String hostFolderName,
208 | //                                                   Path parentFolder, String containerMountPoint ) throws IOException
209 | //    {
210 | //        Path tempFolder = createFolder( hostFolderName, parentFolder );
211 | //        mountHostFolderAsVolume( container, tempFolder, containerMountPoint );
212 | //        return tempFolder;
213 | //    }
214 | 
215 | //    public Path createFolderAndMountAsVolume( GenericContainer container, String containerMountPoint, Path parentFolder ) throws IOException
216 | //    {
217 | //        return null;
218 | //        Path hostFolder = createTempFolder( hostFolderNamePrefix, parentFolder );
219 | //        mountHostFolderAsVolume( container, hostFolder, containerMountPoint );
220 | //        return hostFolder;
221 | //    }
222 | 
223 |     public void mountHostFolderAsVolume(GenericContainer container, Path hostFolder, String containerMountPoint)
224 |     {
225 |         container.withFileSystemBind( hostFolder.toAbsolutePath().toString(),
226 |                                       containerMountPoint,
227 |                                       BindMode.READ_WRITE );
228 |     }
229 | 
230 |     public Path createFolder( String folderName ) throws IOException
231 |     {
232 |     	return createFolder( folderName, methodOutputFolder );
233 |     }
234 | 
235 |     public Path createFolder( String folderName, Path parentFolder ) throws IOException
236 |     {
237 |         if(!parentFolder.startsWith( folderRoot ))
238 |         {
239 |             throw new IOException("Requested to create temp folder outside of " + folderRoot +". " +
240 |                                   "This is a problem with the test.");
241 |         }
242 |         Path hostFolder = parentFolder.resolve(folderName);
243 |         try
244 |         {
245 |             Files.createDirectories( hostFolder );
246 |         }
247 |         catch ( IOException e )
248 |         {
249 |             log.error( "could not create directory: {}", hostFolder.toAbsolutePath() );
250 |             e.printStackTrace();
251 |             throw e;
252 |         }
253 |         log.info( "Created folder {}", hostFolder );
254 |         // flag top level methodOutputFolder for cleanup
255 |         toCompressAfterAll.add( methodOutputFolder ); // toCompressAfterAll is a set, so automatically removes duplicates.
256 |         return hostFolder;
257 |     }
258 | 
259 |     public void setFolderOwnerToCurrentUser(Path file) throws Exception
260 |     {
261 |         setFolderOwnerTo( SetContainerUser.getNonRootUserString(), file );
262 |     }
263 | 
264 |     public void setFolderOwnerToNeo4j(Path file) throws Exception
265 |     {
266 |         setFolderOwnerTo( "7474:7474", file );
267 |     }
268 | 
269 |     protected String getFolderNameFromMountPoint(String containerMountPoint)
270 |     {
271 |         return containerMountPoint.substring( 1 )
272 |                                   .replace( '/', '_' )
273 |                                   .replace( ' ', '_' );
274 |     }
275 | 
276 |     private void setFolderOwnerTo(String userAndGroup, Path... files) throws Exception
277 |     {
278 |         // uses docker privileges to set file owner, since probably the current user is not a sudoer.
279 | 
280 |         // Using nginx because it's easy to verify that the image started.
281 |         try(GenericContainer container = new GenericContainer( DockerImageName.parse( "nginx:latest")))
282 |         {
283 |             container.withExposedPorts( 80 )
284 |                      .waitingFor( Wait.forHttp( "/" ).withStartupTimeout( Duration.ofSeconds( 20 ) ) );
285 |             for(Path p : files)
286 |             {
287 |                 mountHostFolderAsVolume( container, p, p.toAbsolutePath().toString() );
288 |             }
289 |             container.start();
290 |             for(Path p : files)
291 |             {
292 |                 Container.ExecResult x =
293 |                         container.execInContainer( "chown", "-R", userAndGroup,
294 |                                                    p.toAbsolutePath().toString() );
295 |             }
296 |             container.stop();
297 |         }
298 |     }
299 | }
300 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestBasic.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb;
  2 | 
  3 | import com.github.dockerjava.api.command.KillContainerCmd;
  4 | import com.github.dockerjava.api.command.StopContainerCmd;
  5 | import com.neo4j.docker.utils.DatabaseIO;
  6 | import com.neo4j.docker.utils.Neo4jVersion;
  7 | import com.neo4j.docker.utils.TemporaryFolderManager;
  8 | import com.neo4j.docker.utils.TestSettings;
  9 | import com.neo4j.docker.utils.WaitStrategies;
 10 | import org.junit.jupiter.api.Assertions;
 11 | import org.junit.jupiter.api.Assumptions;
 12 | import org.junit.jupiter.api.Test;
 13 | import org.junit.jupiter.api.extension.RegisterExtension;
 14 | import org.junit.jupiter.params.ParameterizedTest;
 15 | import org.junit.jupiter.params.provider.ValueSource;
 16 | import org.slf4j.Logger;
 17 | import org.slf4j.LoggerFactory;
 18 | import org.testcontainers.containers.Container;
 19 | import org.testcontainers.containers.ContainerLaunchException;
 20 | import org.testcontainers.containers.GenericContainer;
 21 | import org.testcontainers.containers.output.OutputFrame;
 22 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 23 | 
 24 | import java.io.IOException;
 25 | import java.nio.file.Files;
 26 | import java.nio.file.Path;
 27 | import java.time.Duration;
 28 | import java.util.List;
 29 | import java.util.stream.Stream;
 30 | 
 31 | import static com.neo4j.docker.utils.Network.getUniqueHostPort;
 32 | import static com.neo4j.docker.utils.WaitStrategies.waitForBoltReady;
 33 | import static com.neo4j.docker.utils.WaitStrategies.waitForNeo4jReady;
 34 | import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
 35 | 
 36 | public class TestBasic
 37 | {
 38 |     private static Logger log = LoggerFactory.getLogger( TestBasic.class );
 39 |     @RegisterExtension
 40 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 41 | 
 42 |     private GenericContainer createBasicContainer()
 43 |     {
 44 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
 45 |         container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 46 |                  .withExposedPorts( 7474, 7687 )
 47 |                  .withLogConsumer( new Slf4jLogConsumer( log ) );
 48 |         return container;
 49 |     }
 50 | 
 51 |     @Test
 52 |     void testListensOn7687()
 53 |     {
 54 |         try ( GenericContainer container = createBasicContainer() )
 55 |         {
 56 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
 57 |             container.start();
 58 |             Assertions.assertTrue( container.isRunning() );
 59 |             String stdout = container.getLogs();
 60 |             Assertions.assertFalse( stdout.contains( "DEBUGGING ENABLED" ),
 61 |                                     "Debugging was enabled even though we did not set debugging" );
 62 |         }
 63 |     }
 64 | 
 65 |     @Test
 66 |     void testNoUnexpectedErrors()
 67 |     {
 68 |         try ( GenericContainer container = createBasicContainer() )
 69 |         {
 70 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
 71 |             container.start();
 72 |             Assertions.assertTrue( container.isRunning() );
 73 | 
 74 |             String stderr = container.getLogs( OutputFrame.OutputType.STDERR );
 75 |             Assertions.assertEquals( "", stderr,
 76 |                                      "Unexpected errors in stderr from container!\n" +
 77 |                                      stderr );
 78 |         }
 79 |     }
 80 | 
 81 |     @Test
 82 |     void testLicenseAcceptanceRequired_Neo4jServer()
 83 |     {
 84 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 3, 0 ) ),
 85 |                                 "No license checks before version 3.3.0" );
 86 |         Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
 87 |                                 "No license checks for community edition" );
 88 | 
 89 |         String logsOut;
 90 |         try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID )
 91 |                 .withLogConsumer( new Slf4jLogConsumer( log ) ) )
 92 |         {
 93 |             WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
 94 |             // container start should fail due to licensing.
 95 |             Assertions.assertThrows( ContainerLaunchException.class, container::start,
 96 |                                      "Neo4j did not notify about accepting the license agreement" );
 97 |             logsOut = container.getLogs();
 98 |         }
 99 |         // double check the container didn't warn and start neo4j anyway
100 |         Assertions.assertTrue( logsOut.contains( "must accept the license" ),
101 |                                "Neo4j did not notify about accepting the license agreement" );
102 |         Assertions.assertFalse( logsOut.contains( "Remote interface available" ),
103 |                                 "Neo4j was started even though the license was not accepted" );
104 |     }
105 | 
106 |     @Test
107 |     void testLicenseAcceptanceAvoidsWarning()
108 |     {
109 |         Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
110 |                                 "No license checks for community edition" );
111 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
112 |                                 "No unified license acceptance method before 5.0.0" );
113 |         try ( GenericContainer container = createBasicContainer() )
114 |         {
115 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
116 |             container.start();
117 |             Assertions.assertTrue( container.isRunning() );
118 | 
119 |             String stdout = container.getLogs( OutputFrame.OutputType.STDOUT );
120 |             Assertions.assertTrue( stdout.contains( "The license agreement was accepted with environment variable " +
121 |                                                     "NEO4J_ACCEPT_LICENSE_AGREEMENT=yes when the Software was started." ),
122 |                                    "Neo4j did not register that the license was agreed to." );
123 |         }
124 |     }
125 | 
126 |     @Test
127 |     void testLicenseAcceptanceAvoidsWarning_evaluation()
128 |     {
129 |         Assumptions.assumeTrue( TestSettings.EDITION == TestSettings.Edition.ENTERPRISE,
130 |                                 "No license checks for community edition" );
131 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
132 |                                 "No unified license acceptance method before 5.0.0" );
133 |         try ( GenericContainer container = createBasicContainer() )
134 |         {
135 |             container.withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "eval" )
136 |                      .waitingFor( waitForNeo4jReady( "neo4j" ) );
137 |             container.start();
138 |             Assertions.assertTrue( container.isRunning() );
139 | 
140 |             String stdout = container.getLogs( OutputFrame.OutputType.STDOUT );
141 |             Assertions.assertTrue( stdout.contains( "The license agreement was accepted with environment variable " +
142 |                                                     "NEO4J_ACCEPT_LICENSE_AGREEMENT=eval when the Software was started." ),
143 |                                    "Neo4j did not register that the evaluation license was agreed to." );
144 |         }
145 |     }
146 | 
147 |     @Test
148 |     void testCypherShellOnPath() throws Exception
149 |     {
150 |         String expectedCypherShellPath = "/var/lib/neo4j/bin/cypher-shell";
151 |         try ( GenericContainer container = createBasicContainer() )
152 |         {
153 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
154 |             container.start();
155 | 
156 |             Container.ExecResult whichResult = container.execInContainer( "which", "cypher-shell" );
157 |             Assertions.assertTrue( whichResult.getStdout().contains( expectedCypherShellPath ),
158 |                                    "cypher-shell not on path" );
159 |         }
160 |     }
161 | 
162 |     @Test
163 |     void testCanChangeWorkDir()
164 |     {
165 |         try ( GenericContainer container = createBasicContainer() )
166 |         {
167 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
168 |             container.setWorkingDirectory( "/tmp" );
169 |             Assertions.assertDoesNotThrow( container::start,
170 |                                            "Could not start neo4j from workdir other than NEO4J_HOME" );
171 |         }
172 |     }
173 | 
174 |     @Test
175 |     void testPackagingInfoContainsDocker() throws Exception
176 |     {
177 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 5, 0, 0 ) ),
178 |                 "No packaging_info file before 5.0.0" );
179 |         try ( GenericContainer container = createBasicContainer() )
180 |         {
181 |             container.waitingFor( waitForNeo4jReady( "neo4j" ) );
182 |             container.start();
183 |             String packagingInfo = container.execInContainer("cat", "/var/lib/neo4j/packaging_info").getStdout();
184 |             List<String> actualPackageType = Stream.of(packagingInfo.split( "\n" ))
185 |                     .filter(line -> line.startsWith("Package Type:"))
186 |                     .toList();
187 |             Assertions.assertEquals(1, actualPackageType.size(),
188 |                     "There should only be 1 Package Type declarations in the packaging_info:\n"+actualPackageType);
189 |             Assertions.assertEquals("Package Type: docker " + TestSettings.BASE_OS.name().toLowerCase(),
190 |                     actualPackageType.get(0), "Docker packaging type is missing from packaging info file");
191 |         }
192 |     }
193 | 
194 |     @ParameterizedTest( name = "ShutsDownCorrectly_{0}" )
195 |     @ValueSource( strings = {"SIGTERM", "SIGINT"} )
196 |     void testShutsDownCleanly( String signal )
197 |     {
198 |         try ( GenericContainer container = createBasicContainer() )
199 |         {
200 |             container.withEnv( "NEO4J_AUTH", "none" )
201 |                      .waitingFor( waitForNeo4jReady( "none" ) );
202 |             container.start();
203 |             DatabaseIO dbio = new DatabaseIO( container );
204 |             dbio.putInitialDataIntoContainer( "neo4j", "none" );
205 |             try(KillContainerCmd kill = container.getDockerClient().killContainerCmd(container.getContainerId());
206 |                 StopContainerCmd stop = container.getDockerClient().stopContainerCmd(container.getContainerId()))
207 |             {
208 |                 log.info( "issuing container stop command " + signal );
209 |                 kill.withSignal(signal).exec();
210 |                 log.info("waiting for container to shut down.");
211 |                 stop.withTimeout(30).exec();
212 |             }
213 |             String stdout = container.getLogs();
214 |             Assertions.assertTrue( stdout.contains( "Neo4j Server shutdown initiated by request" ),
215 |                                    "clean shutdown not initiated by " + signal + "\n" + stdout);
216 |             Assertions.assertTrue( stdout.contains( "Stopped." ),
217 |                                    "clean shutdown not initiated by " + signal + "\n" + stdout);
218 |         }
219 |     }
220 | 
221 |     @Test
222 |     void testStartsWhenDebuggingEnabled()
223 |     {
224 |         try ( GenericContainer container = createBasicContainer() )
225 |         {
226 |             container.withEnv( "NEO4J_DEBUG", "true" );
227 |             container.start();
228 |             Assertions.assertTrue( container.isRunning() );
229 |         }
230 |     }
231 | 
232 |     /*
233 |         This test emulates a termination of the Docker Desktop or Docker Engine by the user. In these
234 |         cases the container receives a SIGKILL signal and neo4j doesn't have time to clean up the PID
235 |         file. In turn this causes the container to not be re-startable.
236 |      */
237 |     @Test
238 |     void testContainerCanBeRestartedAfterUnexpectedTermination() throws IOException
239 |     {
240 |         try ( GenericContainer container = createBasicContainer() )
241 |         {
242 |             int boltHostPort = getUniqueHostPort();
243 |             int browserHostPort = getUniqueHostPort();
244 | 
245 |             container.waitingFor( waitForBoltReady() );
246 |             container.withEnv( "NEO4J_AUTH", "none" );
247 | 
248 |             // Ensuring host ports are constant with container restarts
249 |             container.setPortBindings( List.of( browserHostPort + ":7474", boltHostPort + ":7687" ) );
250 | 
251 |             container.start();
252 | 
253 |             // Terminating container with a SIGKILL signal to emulate docker engine (docker desktop) being terminated by user.
254 |             // This also keeps around the container unlike GenericContainer::stop(), which cleans up everything
255 |             log.info( "Terminating container with SIGKILL signal" );
256 |             container.getDockerClient().killContainerCmd( container.getContainerId() ).withSignal( "SIGKILL" ).exec();
257 | 
258 |             await().atMost( Duration.ofSeconds( 120 ) ).untilAsserted( () -> {
259 |                 Assertions.assertFalse( container.isRunning());
260 |             } );
261 | 
262 |             // Restarting the container with DockerClient because the GenericContainer was not terminates and GenericContainer::start()
263 |             // does not work
264 |             log.info( "Starting container" );
265 |             container.getDockerClient().startContainerCmd( container.getContainerId() ).exec();
266 | 
267 |             // Applying the Waiting strategy to ensure container is correctly running, because DockerClient does not check
268 |             waitForBoltReady().waitUntilReady( container );
269 |         }
270 |     }
271 | 
272 |     @Test
273 |     void testExtensionScriptIsExecuted() throws IOException
274 |     {
275 |         Path scriptFolder = temporaryFolderManager.createFolder("extension_script");
276 |         Path script = scriptFolder.resolve("startscript.sh");
277 |         Files.writeString(script, "#!/bin/bash\n\necho \"SCRIPT EXECUTED!\"");
278 | 
279 |         try ( GenericContainer container = createBasicContainer() )
280 |         {
281 |             temporaryFolderManager.mountHostFolderAsVolume(container, scriptFolder, "/extension");
282 |             container.waitingFor(waitForBoltReady())
283 |                     .withEnv("EXTENSION_SCRIPT", "/extension/startscript.sh");
284 |             container.start();
285 |             String logs = container.getLogs(OutputFrame.OutputType.STDOUT);
286 |             Assertions.assertTrue(logs.contains("SCRIPT EXECUTED!"), "The extension script did not get executed");
287 |         }
288 |     }
289 | }
290 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/plugins/TestPluginInstallation.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb.plugins;
  2 | 
  3 | import com.github.dockerjava.api.command.CreateContainerCmd;
  4 | import com.neo4j.docker.coredb.configurations.Configuration;
  5 | import com.neo4j.docker.coredb.configurations.Setting;
  6 | import com.neo4j.docker.utils.DatabaseIO;
  7 | import com.neo4j.docker.utils.HttpServerTestExtension;
  8 | import com.neo4j.docker.utils.Neo4jVersion;
  9 | import com.neo4j.docker.utils.SetContainerUser;
 10 | import com.neo4j.docker.utils.TemporaryFolderManager;
 11 | import com.neo4j.docker.utils.TestSettings;
 12 | import com.neo4j.docker.utils.WaitStrategies;
 13 | import org.junit.jupiter.api.Assertions;
 14 | import org.junit.jupiter.api.Assumptions;
 15 | import org.junit.jupiter.api.Test;
 16 | import org.junit.jupiter.api.extension.RegisterExtension;
 17 | import org.junit.jupiter.params.ParameterizedTest;
 18 | import org.junit.jupiter.params.provider.ValueSource;
 19 | import org.neo4j.driver.Record;
 20 | import org.slf4j.Logger;
 21 | import org.slf4j.LoggerFactory;
 22 | import org.testcontainers.Testcontainers;
 23 | import org.testcontainers.containers.ContainerLaunchException;
 24 | import org.testcontainers.containers.GenericContainer;
 25 | import org.testcontainers.containers.Network;
 26 | import org.testcontainers.containers.output.OutputFrame;
 27 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 28 | 
 29 | import java.nio.file.Path;
 30 | import java.time.Duration;
 31 | import java.util.List;
 32 | import java.util.function.Consumer;
 33 | 
 34 | import static com.neo4j.docker.utils.TestSettings.NEO4J_VERSION;
 35 | 
 36 | 
 37 | public class TestPluginInstallation
 38 | {
 39 |     private static final String DB_USER = "neo4j";
 40 |     private static final String DB_PASSWORD = "qualityPassword";
 41 | 
 42 |     private final Logger log = LoggerFactory.getLogger( TestPluginInstallation.class );
 43 |     @RegisterExtension
 44 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 45 |     @RegisterExtension
 46 |     public HttpServerTestExtension httpServer = new HttpServerTestExtension();
 47 |     StubPluginHelper stubPluginHelper = new StubPluginHelper(httpServer);
 48 | 
 49 | 
 50 |     private GenericContainer createContainerWithTestingPlugin(boolean asCurrentUser)
 51 |     {
 52 |         Testcontainers.exposeHostPorts( httpServer.PORT );
 53 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
 54 | 
 55 |         container.withEnv( "NEO4J_AUTH", DB_USER + "/" + DB_PASSWORD )
 56 |                  .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 57 |                  .withEnv( "NEO4J_DEBUG", "yes" )
 58 |                  .withEnv( Neo4jPluginEnv.get(), "[\"" + stubPluginHelper.PLUGIN_ENV_NAME + "\"]" )
 59 |                  .withExposedPorts( 7474, 7687 )
 60 |                  .withLogConsumer( new Slf4jLogConsumer( log ) )
 61 |                  .waitingFor( WaitStrategies.waitForNeo4jReady( DB_PASSWORD));
 62 |         if(asCurrentUser) SetContainerUser.nonRootUser( container );
 63 |         return container;
 64 |     }
 65 | 
 66 |     @ParameterizedTest(name = "as_current_user_{0}")
 67 |     @ValueSource( booleans = {true, false} )
 68 |     public void testPluginLoads(boolean asCurrentUser) throws Exception
 69 |     {
 70 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
 71 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
 72 |         try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser) )
 73 |         {
 74 |             container.start();
 75 |             DatabaseIO db = new DatabaseIO( container );
 76 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
 77 |         }
 78 |     }
 79 | 
 80 |     @Test
 81 |     public void test_NEO4JLABS_PLUGIN_envWorksIn5() throws Exception
 82 |     {
 83 |         Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_500 ),
 84 |                                 "NEO4JLABS_PLUGIN backwards compatibility does not need checking pre 5.x" );
 85 | 
 86 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
 87 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
 88 |         try ( GenericContainer container = createContainerWithTestingPlugin(false) )
 89 |         {
 90 |             container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "" );
 91 |             container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "[\"_testing\"]" );
 92 |             container.start();
 93 |             DatabaseIO db = new DatabaseIO( container );
 94 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
 95 |         }
 96 |     }
 97 | 
 98 |     @Test
 99 |     public void test_NEO4J_PLUGIN_envWorksIn44() throws Exception
100 |     {
101 |         Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 4, 18 ) ),
102 |                                 "NEO4JLABS_PLUGIN did not work in 4.4 before 4.4.18" );
103 |         Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
104 |                                 "Only checking forwards compatibility in 4.4" );
105 | 
106 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
107 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
108 |         try ( GenericContainer container = createContainerWithTestingPlugin(false) )
109 |         {
110 |             container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_5X, "[\"_testing\"]" );
111 |             container.withEnv( Neo4jPluginEnv.PLUGIN_ENV_4X, "" );
112 |             container.start();
113 |             DatabaseIO db = new DatabaseIO( container );
114 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
115 |         }
116 |     }
117 | 
118 |     @Test
119 |     public void testPluginConfigurationDoesNotOverrideUserSetValues() throws Exception
120 |     {
121 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
122 |         Configuration securityProcedures = Configuration.getConfigurationNameMap()
123 |                 .get( Setting.SECURITY_PROCEDURES_UNRESTRICTED );
124 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
125 |         try ( GenericContainer container = createContainerWithTestingPlugin(false) )
126 |         {
127 |             container.withEnv( securityProcedures.envName, "foo" );
128 |             container.start();
129 |             // When we connect to the database with the plugin
130 |             // Check that the config remains as set by our env var and is not overridden by the plugin defaults
131 |             DatabaseIO db = new DatabaseIO( container );
132 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
133 |             db.verifyConfigurationSetting( DB_USER, DB_PASSWORD,
134 |                                            securityProcedures,
135 |                                            "foo",
136 |                                            "neo4j config should not be overridden by plugin" );
137 |         }
138 |     }
139 | 
140 |     @Test
141 |     void invalidPluginNameShouldGiveOptionsAndError()
142 |     {
143 |         Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
144 |         try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) )
145 |         {
146 |             // if we try to set a plugin that doesn't exist
147 |             container.withEnv( Neo4jPluginEnv.get(), "[\"notarealplugin\"]" )
148 |                      .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
149 |                      .withLogConsumer( new Slf4jLogConsumer( log ) );
150 |             WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
151 |             Assertions.assertThrows( ContainerLaunchException.class, container::start );
152 |             // the container should output a helpful message and quit
153 |             String stdout = container.getLogs();
154 |             Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) );
155 |             Assertions.assertFalse( stdout.contains( "_testing" ), "Fake _testing plugin is exposed." );
156 |         }
157 |     }
158 | 
159 |     @Test
160 |     void invalidPluginNameShouldGiveOptionsAndError_mulitpleplugins()
161 |     {
162 |         Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
163 |         try ( GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID ) )
164 |         {
165 |             // if we try to set a plugin that doesn't exist
166 |             container.withEnv( Neo4jPluginEnv.get(), "[\"apoc\", \"notarealplugin\"]" )
167 |                      .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
168 |                      .withLogConsumer( new Slf4jLogConsumer( log ) );
169 |             WaitStrategies.waitUntilContainerFinished( container, Duration.ofSeconds( 30 ) );
170 |             Assertions.assertThrows( ContainerLaunchException.class, container::start );
171 |             // the container should output a helpful message and quit
172 |             String stdout = container.getLogs();
173 |             Assertions.assertTrue( stdout.contains( "\"notarealplugin\" is not a known Neo4j plugin. Options are:" ) );
174 |             Assertions.assertFalse( stdout.contains( StubPluginHelper.PLUGIN_ENV_NAME), "Fake _testing plugin is exposed." );
175 |         }
176 |     }
177 | 
178 |     @Test
179 |     public void testBrokenVersionsJsonGivesWarning() throws Exception
180 |     {
181 |         Assumptions.assumeTrue( NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_440 ) );
182 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
183 |         // create a versions.json that DOES NOT contain the current neo4j version in its mapping
184 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, new Neo4jVersion(50,0,0));
185 |         try ( GenericContainer container = createContainerWithTestingPlugin(true) )
186 |         {
187 |             container.start();
188 |             String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR );
189 |             Assertions.assertTrue( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j " + NEO4J_VERSION ),
190 |                                    "Did not error about plugin compatibility." );
191 |             DatabaseIO db = new DatabaseIO( container );
192 |             // make sure plugin did not load
193 |             List<Record> procedures = db.runCypherQuery( DB_USER, DB_PASSWORD,
194 |                                                          "SHOW PROCEDURES YIELD name, signature RETURN name, signature" );
195 |             Assertions.assertFalse( procedures.stream()
196 |                                               .anyMatch( x -> x.get( "name" ).asString()
197 |                                                                .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ),
198 |                                     "Incompatible test plugin was loaded." );
199 |         }
200 |     }
201 | 
202 |     //@Disabled("Test is flaky for unknown reasons. Needs further investigation.")
203 |     @Test
204 |     void testMissingVersionsJsonGivesWarning()
205 |     {
206 |         Configuration securityProcedures = Configuration.getConfigurationNameMap().get(Setting.SECURITY_PROCEDURES_UNRESTRICTED);
207 |         // make double sure there are no versions.json files being served.
208 |         httpServer.unregisterEndpoint("/versions.json");
209 |         try ( Network net = Network.newNetwork();
210 |               GenericContainer container = createContainerWithTestingPlugin(false)
211 |                       .withNetwork(net) ) {
212 |             container.start();
213 |             String startupErrors = container.getLogs( OutputFrame.OutputType.STDERR );
214 |             Assertions.assertTrue(
215 |                     startupErrors.contains( "could not query http://host.testcontainers.internal:3000/versions.json for plugin compatibility information" ),
216 |                     "Did not error about missing versions.json. Actual errors:\n\"" + startupErrors + "\"" );
217 |             Assertions.assertFalse( startupErrors.contains( "No compatible \"_testing\" plugin found for Neo4j " + NEO4J_VERSION ),
218 |                                     "Should not have errored about incompatible versions in versions.json" );
219 |             // make sure plugin did not load
220 |             DatabaseIO db = new DatabaseIO( container );
221 |             List<Record> procedures = db.runCypherQuery( DB_USER, DB_PASSWORD,
222 |                                                          "SHOW PROCEDURES YIELD name, signature RETURN name, signature" );
223 |             Assertions.assertFalse( procedures.stream()
224 |                                               .anyMatch( x -> x.get( "name" ).asString()
225 |                                                                .equals( "com.neo4j.docker.test.myplugin.defaultValues" ) ),
226 |                                     "Incompatible test plugin was loaded." );
227 |             // make sure configuration did not set
228 |             String securityConf = db.getConfigurationSettingAsString(DB_USER, DB_PASSWORD, securityProcedures);
229 |             Assertions.assertFalse(securityConf.contains("com.neo4j.docker.neo4jserver.plugins.*"),
230 |                     "Test plugin configuration setting was set, even though the plugin did not load.");
231 |         }
232 |     }
233 | 
234 |     @ParameterizedTest(name = "as_current_user_{0}")
235 |     @ValueSource( booleans = {true, false} )
236 |     public void testPlugin_originalEntrypointLocation(boolean asCurrentUser) throws Exception
237 |     {
238 |         // Older versions of Neo4j had docker-entrypoint.sh in / rather than /startup and sometimes
239 |         // users use the old entrypoint location. This apparently caused problems loading plugins.
240 |         Assumptions.assumeTrue( NEO4J_VERSION.isOlderThan( Neo4jVersion.NEO4J_VERSION_500 ),
241 |                                 "/docker-entrypoint.sh is permanently moved from 5.0 onwards" );
242 |         Path pluginsDir = temporaryFolderManager.createFolder("plugins");
243 |         stubPluginHelper.createStubPluginForVersion(pluginsDir, NEO4J_VERSION);
244 |         try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser) )
245 |         {
246 |             container.withCreateContainerCmdModifier(
247 |                     (Consumer<CreateContainerCmd>) cmd -> cmd.withEntrypoint( "/docker-entrypoint.sh", "neo4j" ) );
248 |             container.start();
249 |             DatabaseIO db = new DatabaseIO( container );
250 |             stubPluginHelper.verifyStubPluginLoaded( db, DB_USER, DB_PASSWORD );
251 |         }
252 |     }
253 | 
254 |     @ParameterizedTest( name = "as_current_user_{0}" )
255 |     @ValueSource( booleans = {true, false} )
256 |     void testPluginIsMovedToMountedFolderAndIsLoadedCorrectly( boolean asCurrentUser ) throws Exception
257 |     {
258 |         try ( GenericContainer container = createContainerWithTestingPlugin(asCurrentUser))
259 |         {
260 |             Path pluginsFolder = temporaryFolderManager.createFolderAndMountAsVolume(container, "/plugins");
261 |             stubPluginHelper.createStubPluginForVersion(pluginsFolder, NEO4J_VERSION);
262 |             container.start();
263 |             Assertions.assertTrue( pluginsFolder.resolve( StubPluginHelper.PLUGIN_ENV_NAME +".jar" ).toFile().exists(),
264 |                     "Did not find _testing.jar in plugins folder");
265 | 
266 |             DatabaseIO databaseIO = new DatabaseIO( container );
267 |             stubPluginHelper.verifyStubPluginLoaded(databaseIO, DB_USER, DB_PASSWORD);
268 |         }
269 |     }
270 | }
271 | 
```

--------------------------------------------------------------------------------
/neo4j/docker-neo4j/src/test/java/com/neo4j/docker/coredb/TestMounting.java:
--------------------------------------------------------------------------------

```java
  1 | package com.neo4j.docker.coredb;
  2 | 
  3 | import com.github.dockerjava.api.command.CreateContainerCmd;
  4 | import com.github.dockerjava.api.model.Bind;
  5 | import com.neo4j.docker.utils.DatabaseIO;
  6 | import com.neo4j.docker.utils.Neo4jVersion;
  7 | import com.neo4j.docker.utils.SetContainerUser;
  8 | import com.neo4j.docker.utils.TemporaryFolderManager;
  9 | import com.neo4j.docker.utils.TestSettings;
 10 | import com.neo4j.docker.utils.WaitStrategies;
 11 | import org.junit.jupiter.api.AfterEach;
 12 | import org.junit.jupiter.api.Assertions;
 13 | import org.junit.jupiter.api.Assumptions;
 14 | import org.junit.jupiter.api.Test;
 15 | import org.junit.jupiter.api.extension.RegisterExtension;
 16 | import org.junit.jupiter.params.ParameterizedTest;
 17 | import org.junit.jupiter.params.provider.Arguments;
 18 | import org.junit.jupiter.params.provider.MethodSource;
 19 | import org.junit.jupiter.params.provider.ValueSource;
 20 | import org.slf4j.Logger;
 21 | import org.slf4j.LoggerFactory;
 22 | import org.testcontainers.containers.ContainerLaunchException;
 23 | import org.testcontainers.containers.GenericContainer;
 24 | import org.testcontainers.containers.output.OutputFrame;
 25 | import org.testcontainers.containers.output.Slf4jLogConsumer;
 26 | import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
 27 | import org.testcontainers.containers.wait.strategy.Wait;
 28 | 
 29 | import java.io.File;
 30 | import java.io.IOException;
 31 | import java.nio.file.Files;
 32 | import java.nio.file.Path;
 33 | import java.time.Duration;
 34 | import java.util.Random;
 35 | import java.util.function.Consumer;
 36 | import java.util.stream.Stream;
 37 | 
 38 | public class TestMounting
 39 | {
 40 |     private static Logger log = LoggerFactory.getLogger( TestMounting.class );
 41 | 
 42 |     @RegisterExtension
 43 |     public static TemporaryFolderManager temporaryFolderManager = new TemporaryFolderManager();
 44 | 
 45 |     @AfterEach
 46 |     void archiveTestArtifacts() throws Exception
 47 |     {
 48 |         temporaryFolderManager.triggerCleanup();
 49 |     }
 50 | 
 51 |     static Stream<Arguments> defaultUserFlagSecurePermissionsFlag()
 52 |     {
 53 |         // "asUser={0}, secureFlag={1}"
 54 |         // expected behaviour is that if you set --user flag, your data should be read/writable
 55 |         // if you don't set --user flag then read/writability should be controlled by the secure file permissions flag
 56 |         // the asCurrentUser=false, secureflag=true combination is tested separately because the container should fail to start.
 57 |         return Stream.of(
 58 |                 Arguments.arguments( false, false ),
 59 |                 Arguments.arguments( true, false ),
 60 |                 Arguments.arguments( true, true ) );
 61 |     }
 62 | 
 63 |     private GenericContainer setupBasicContainer( boolean asCurrentUser, boolean isSecurityFlagSet )
 64 |     {
 65 |         log.info( "Running as user {}, {}",
 66 |                   asCurrentUser ? "non-root" : "root",
 67 |                   isSecurityFlagSet ? "with secure file permissions" : "with unsecured file permissions" );
 68 | 
 69 |         GenericContainer container = new GenericContainer( TestSettings.IMAGE_ID );
 70 |         container.withExposedPorts( 7474, 7687 )
 71 |                  .withLogConsumer( new Slf4jLogConsumer( log ) )
 72 |                  .withEnv( "NEO4J_ACCEPT_LICENSE_AGREEMENT", "yes" )
 73 |                  .withEnv( "NEO4J_AUTH", "none" )
 74 |                  .waitingFor( WaitStrategies.waitForNeo4jReady( "none" ) );
 75 |         if ( asCurrentUser )
 76 |         {
 77 |             SetContainerUser.nonRootUser( container );
 78 |         }
 79 |         if ( isSecurityFlagSet )
 80 |         {
 81 |             container.withEnv( "SECURE_FILE_PERMISSIONS", "yes" );
 82 |         }
 83 |         return container;
 84 |     }
 85 | 
 86 |     private void verifySingleFolder( Path folderToCheck, boolean shouldBeWritable )
 87 |     {
 88 |         String folderForDiagnostics = folderToCheck.toAbsolutePath().toString();
 89 | 
 90 |         Assertions.assertTrue( folderToCheck.toFile().exists(), "did not create " + folderForDiagnostics + " folder on host" );
 91 |         if ( shouldBeWritable )
 92 |         {
 93 |             Assertions.assertTrue( folderToCheck.toFile().canRead(), "cannot read host " + folderForDiagnostics + " folder" );
 94 |             Assertions.assertTrue( folderToCheck.toFile().canWrite(), "cannot write to host " + folderForDiagnostics + " folder" );
 95 |         }
 96 |     }
 97 | 
 98 |     private void verifyDataFolderContentsArePresentOnHost( Path dataMount, boolean shouldBeWritable )
 99 |     {
100 |         verifySingleFolder( dataMount.resolve( "databases" ), shouldBeWritable );
101 | 
102 |         if ( TestSettings.NEO4J_VERSION.isAtLeastVersion( Neo4jVersion.NEO4J_VERSION_400 ) )
103 |         {
104 |             verifySingleFolder( dataMount.resolve( "transactions" ), shouldBeWritable );
105 |         }
106 |     }
107 | 
108 |     private void verifyLogsFolderContentsArePresentOnHost( Path logsMount, boolean shouldBeWritable )
109 |     {
110 |         verifySingleFolder( logsMount, shouldBeWritable );
111 |         Assertions.assertTrue( logsMount.resolve( "debug.log" ).toFile().exists(),
112 |                                "Neo4j did not write a debug.log file to " + logsMount.toString() );
113 |         Assertions.assertEquals( shouldBeWritable,
114 |                                  logsMount.resolve( "debug.log" ).toFile().canWrite(),
115 |                                  String.format( "The debug.log file should %sbe writable", shouldBeWritable ? "" : "not " ) );
116 |     }
117 | 
118 |     @ParameterizedTest(name = "as_current_user_{0}")
119 |     @ValueSource( booleans = {true, false} )
120 |     void canDumpConfig( boolean asCurrentUser ) throws Exception
121 |     {
122 |         File confFile;
123 |         Path confMount;
124 |         String assertMsg = "Conf file was not successfully dumped when running container as "
125 |                            + (asCurrentUser? "current user" : "root");
126 | 
127 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
128 |         {
129 |             //Mount /conf
130 |             confMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
131 |             confFile = confMount.resolve( "neo4j.conf" ).toFile();
132 | 
133 |             //Start the container
134 |             container.setWaitStrategy(
135 |                     Wait.forLogMessage( ".*Config Dumped.*", 1 )
136 |                         .withStartupTimeout( Duration.ofSeconds( 30 ) ) );
137 |             container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() );
138 |             container.setCommand( "dump-config" );
139 |             container.start();
140 |         }
141 | 
142 |         // verify conf file was written
143 |         Assertions.assertTrue( confFile.exists(), assertMsg );
144 |         // verify conf folder does not have new owner if not running as root
145 |         if ( asCurrentUser )
146 |         {
147 |             int fileUID = (Integer) Files.getAttribute( confFile.toPath(), "unix:uid" );
148 |             int expectedUID = Integer.parseInt( SetContainerUser.getNonRootUserString().split( ":" )[0] );
149 |             Assertions.assertEquals( expectedUID, fileUID, "Owner of dumped conf file is not the currently running user" );
150 |         }
151 |     }
152 | 
153 |     @Test
154 |     void canDumpConfig_errorsWithoutConfMount() throws Exception
155 |     {
156 |         try ( GenericContainer container = setupBasicContainer( false, false ) )
157 |         {
158 |             container.setWaitStrategy(
159 |                     Wait.forLogMessage( ".*Config Dumped.*", 1 )
160 |                         .withStartupTimeout( Duration.ofSeconds( 30 ) ) );
161 |             container.setStartupCheckStrategy( new OneShotStartupCheckStrategy() );
162 |             container.setCommand( "dump-config" );
163 |             Assertions.assertThrows( ContainerLaunchException.class,
164 |                                      () -> container.start(),
165 |                                      "Did not error when dump config requested without mounted /conf folder" );
166 |             String stderr = container.getLogs( OutputFrame.OutputType.STDERR );
167 |             Assertions.assertTrue( stderr.endsWith( "You must mount a folder to /conf so that the configuration file(s) can be dumped to there.\n" ) );
168 |         }
169 |     }
170 | 
171 |     @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
172 |     @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
173 |     void testCanMountJustDataFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
174 |     {
175 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
176 |                                 "User checks not valid before 3.1" );
177 | 
178 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
179 |         {
180 |             Path dataMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
181 |             container.start();
182 | 
183 |             // neo4j should now have started, so there'll be stuff in the data folder
184 |             // we need to check that stuff is readable and owned by the correct user
185 |             verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser );
186 |         }
187 |     }
188 | 
189 |     @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
190 |     @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
191 |     void testCanMountJustLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
192 |     {
193 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
194 |                                 "User checks not valid before 3.1" );
195 | 
196 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
197 |         {
198 |             Path logsMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
199 |             container.start();
200 | 
201 |             verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser );
202 |         }
203 |     }
204 | 
205 |     @ParameterizedTest( name = "asUser={0}, secureFlag={1}" )
206 |     @MethodSource( "defaultUserFlagSecurePermissionsFlag" )
207 |     void testCanMountDataAndLogsFolder( boolean asCurrentUser, boolean isSecurityFlagSet ) throws IOException
208 |     {
209 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
210 |                                 "User checks not valid before 3.1" );
211 | 
212 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, isSecurityFlagSet ) )
213 |         {
214 |             Path dataMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
215 |             Path logsMount = temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
216 |             container.start();
217 | 
218 |             verifyDataFolderContentsArePresentOnHost( dataMount, asCurrentUser );
219 |             verifyLogsFolderContentsArePresentOnHost( logsMount, asCurrentUser );
220 |         }
221 |     }
222 | 
223 |     @Test
224 |     void testCantWriteIfSecureEnabledAndNoPermissions_data() throws IOException
225 |     {
226 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
227 |                                 "User checks not valid before 3.1" );
228 | 
229 |         try ( GenericContainer container = setupBasicContainer( false, true ) )
230 |         {
231 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
232 | 
233 |             // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting
234 |             container.setWaitStrategy( Wait.forLogMessage( "[fF]older /data is not accessible for user", 1 )
235 |                                            .withStartupTimeout( Duration.ofSeconds( 20 ) ) );
236 |             Assertions.assertThrows( ContainerLaunchException.class,
237 |                                      () -> container.start(),
238 |                                      "Neo4j should not start in secure mode if data folder is unwritable" );
239 |         }
240 |     }
241 | 
242 |     @Test
243 |     void testCantWriteIfSecureEnabledAndNoPermissions_logs() throws IOException
244 |     {
245 |         Assumptions.assumeTrue( TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 3, 1, 0 ) ),
246 |                                 "User checks not valid before 3.1" );
247 | 
248 |         try ( GenericContainer container = setupBasicContainer( false, true ) )
249 |         {
250 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
251 | 
252 |             // currently Neo4j will try to start and fail. It should be fixed to throw an error and not try starting
253 |             container.setWaitStrategy( Wait.forLogMessage( "[fF]older /logs is not accessible for user", 1 )
254 |                                            .withStartupTimeout( Duration.ofSeconds( 20 ) ) );
255 |             Assertions.assertThrows( ContainerLaunchException.class,
256 |                                      () -> container.start(),
257 |                                      "Neo4j should not start in secure mode if logs folder is unwritable" );
258 |         }
259 |     }
260 | 
261 |     @ParameterizedTest(name = "as_current_user_{0}")
262 |     @ValueSource( booleans = {true, false} )
263 |     void canMountAllTheThings_fileMounts( boolean asCurrentUser ) throws Exception
264 |     {
265 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
266 |         {
267 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/conf");
268 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/data");
269 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/import");
270 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/logs");
271 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/metrics");
272 |             temporaryFolderManager.createFolderAndMountAsVolume(container, "/plugins");
273 |             container.start();
274 |             DatabaseIO databaseIO = new DatabaseIO( container );
275 |             // do some database writes so that we try writing to writable folders.
276 |             databaseIO.putInitialDataIntoContainer( "neo4j", "none" );
277 |             databaseIO.verifyInitialDataInContainer( "neo4j", "none" );
278 |         }
279 |     }
280 | 
281 |     @ParameterizedTest(name = "as_current_user_{0}")
282 |     @ValueSource( booleans = {true, false} )
283 |     void canMountAllTheThings_namedVolumes( boolean asCurrentUser ) throws Exception
284 |     {
285 |         String id = String.format( "%04d", new Random().nextInt( 10000 ) );
286 |         try ( GenericContainer container = setupBasicContainer( asCurrentUser, false ) )
287 |         {
288 |             container.withCreateContainerCmdModifier(
289 |                     (Consumer<CreateContainerCmd>) cmd -> cmd.getHostConfig().withBinds(
290 |                             Bind.parse( "conf-" + id + ":/conf" ),
291 |                             Bind.parse( "data-" + id + ":/data" ),
292 |                             Bind.parse( "import-" + id + ":/import" ),
293 |                             Bind.parse( "logs-" + id + ":/logs" ),
294 |                             //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
295 |                             Bind.parse( "plugins-" + id + ":/plugins" )
296 |                     ) );
297 |             container.start();
298 |             DatabaseIO databaseIO = new DatabaseIO( container );
299 |             // do some database writes so that we try writing to writable folders.
300 |             databaseIO.putInitialDataIntoContainer( "neo4j", "none" );
301 |             databaseIO.verifyInitialDataInContainer( "neo4j", "none" );
302 |         }
303 |     }
304 | 
305 |     @Test
306 |     void shouldReownSubfilesToNeo4j() throws Exception
307 |     {
308 |         Assumptions.assumeTrue(
309 |                 TestSettings.NEO4J_VERSION.isAtLeastVersion( new Neo4jVersion( 4, 0, 0 ) ),
310 |                 "User checks not valid before 4.0" );
311 | 
312 |         Path logMount = temporaryFolderManager.createFolder( "subfileownership" );
313 |         Path debugLog = logMount.resolve( "debug.log" );
314 |         // put file in logMount
315 |         Files.write( debugLog, "some log words".getBytes() );
316 |         // make neo4j own the conf folder but NOT the neo4j.conf
317 |         temporaryFolderManager.setFolderOwnerToNeo4j( logMount );
318 |         temporaryFolderManager.setFolderOwnerToCurrentUser( debugLog );
319 | 
320 |         try ( GenericContainer container = setupBasicContainer( false, false ) )
321 |         {
322 |             temporaryFolderManager.mountHostFolderAsVolume( container, logMount, "/logs" );
323 |             container.start();
324 |             // if debug.log doesn't get re-owned, neo4j will not start and this test will fail here
325 |         }
326 |     }
327 | }
328 | 
```

--------------------------------------------------------------------------------
/knowledge_graphs/query_knowledge_graph.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Knowledge Graph Query Tool
  4 | 
  5 | Interactive script to explore what's actually stored in your Neo4j knowledge graph.
  6 | Useful for debugging hallucination detection and understanding graph contents.
  7 | """
  8 | 
  9 | import asyncio
 10 | import os
 11 | from dotenv import load_dotenv
 12 | from neo4j import AsyncGraphDatabase
 13 | from typing import List, Dict, Any
 14 | import argparse
 15 | 
 16 | 
 17 | class KnowledgeGraphQuerier:
 18 |     """Interactive tool to query the knowledge graph"""
 19 |     
 20 |     def __init__(self, neo4j_uri: str, neo4j_user: str, neo4j_password: str):
 21 |         self.neo4j_uri = neo4j_uri
 22 |         self.neo4j_user = neo4j_user
 23 |         self.neo4j_password = neo4j_password
 24 |         self.driver = None
 25 |     
 26 |     async def initialize(self):
 27 |         """Initialize Neo4j connection"""
 28 |         self.driver = AsyncGraphDatabase.driver(
 29 |             self.neo4j_uri, 
 30 |             auth=(self.neo4j_user, self.neo4j_password)
 31 |         )
 32 |         print("🔗 Connected to Neo4j knowledge graph")
 33 |     
 34 |     async def close(self):
 35 |         """Close Neo4j connection"""
 36 |         if self.driver:
 37 |             await self.driver.close()
 38 |     
 39 |     async def list_repositories(self):
 40 |         """List all repositories in the knowledge graph"""
 41 |         print("\n📚 Repositories in Knowledge Graph:")
 42 |         print("=" * 50)
 43 |         
 44 |         async with self.driver.session() as session:
 45 |             query = "MATCH (r:Repository) RETURN r.name as name ORDER BY r.name"
 46 |             result = await session.run(query)
 47 |             
 48 |             repos = []
 49 |             async for record in result:
 50 |                 repos.append(record['name'])
 51 |             
 52 |             if repos:
 53 |                 for i, repo in enumerate(repos, 1):
 54 |                     print(f"{i}. {repo}")
 55 |             else:
 56 |                 print("No repositories found in knowledge graph.")
 57 |         
 58 |         return repos
 59 |     
 60 |     async def explore_repository(self, repo_name: str):
 61 |         """Get overview of a specific repository"""
 62 |         print(f"\n🔍 Exploring Repository: {repo_name}")
 63 |         print("=" * 60)
 64 |         
 65 |         async with self.driver.session() as session:
 66 |             # Get file count
 67 |             files_query = """
 68 |             MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)
 69 |             RETURN count(f) as file_count
 70 |             """
 71 |             result = await session.run(files_query, repo_name=repo_name)
 72 |             file_count = (await result.single())['file_count']
 73 |             
 74 |             # Get class count
 75 |             classes_query = """
 76 |             MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(c:Class)
 77 |             RETURN count(DISTINCT c) as class_count
 78 |             """
 79 |             result = await session.run(classes_query, repo_name=repo_name)
 80 |             class_count = (await result.single())['class_count']
 81 |             
 82 |             # Get function count
 83 |             functions_query = """
 84 |             MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(func:Function)
 85 |             RETURN count(DISTINCT func) as function_count
 86 |             """
 87 |             result = await session.run(functions_query, repo_name=repo_name)
 88 |             function_count = (await result.single())['function_count']
 89 |             
 90 |             print(f"📄 Files: {file_count}")
 91 |             print(f"🏗️  Classes: {class_count}")
 92 |             print(f"⚙️  Functions: {function_count}")
 93 |     
 94 |     async def list_classes(self, repo_name: str = None, limit: int = 20):
 95 |         """List classes in the knowledge graph"""
 96 |         title = f"Classes in {repo_name}" if repo_name else "All Classes"
 97 |         print(f"\n🏗️  {title} (limit {limit}):")
 98 |         print("=" * 50)
 99 |         
100 |         async with self.driver.session() as session:
101 |             if repo_name:
102 |                 query = """
103 |                 MATCH (r:Repository {name: $repo_name})-[:CONTAINS]->(f:File)-[:DEFINES]->(c:Class)
104 |                 RETURN c.name as name, c.full_name as full_name
105 |                 ORDER BY c.name
106 |                 LIMIT $limit
107 |                 """
108 |                 result = await session.run(query, repo_name=repo_name, limit=limit)
109 |             else:
110 |                 query = """
111 |                 MATCH (c:Class)
112 |                 RETURN c.name as name, c.full_name as full_name
113 |                 ORDER BY c.name
114 |                 LIMIT $limit
115 |                 """
116 |                 result = await session.run(query, limit=limit)
117 |             
118 |             classes = []
119 |             async for record in result:
120 |                 classes.append({
121 |                     'name': record['name'],
122 |                     'full_name': record['full_name']
123 |                 })
124 |             
125 |             if classes:
126 |                 for i, cls in enumerate(classes, 1):
127 |                     print(f"{i:2d}. {cls['name']} ({cls['full_name']})")
128 |             else:
129 |                 print("No classes found.")
130 |         
131 |         return classes
132 |     
133 |     async def explore_class(self, class_name: str):
134 |         """Get detailed information about a specific class"""
135 |         print(f"\n🔍 Exploring Class: {class_name}")
136 |         print("=" * 60)
137 |         
138 |         async with self.driver.session() as session:
139 |             # Find the class
140 |             class_query = """
141 |             MATCH (c:Class)
142 |             WHERE c.name = $class_name OR c.full_name = $class_name
143 |             RETURN c.name as name, c.full_name as full_name
144 |             LIMIT 1
145 |             """
146 |             result = await session.run(class_query, class_name=class_name)
147 |             class_record = await result.single()
148 |             
149 |             if not class_record:
150 |                 print(f"❌ Class '{class_name}' not found in knowledge graph.")
151 |                 return None
152 |             
153 |             actual_name = class_record['name']
154 |             full_name = class_record['full_name']
155 |             
156 |             print(f"📋 Name: {actual_name}")
157 |             print(f"📋 Full Name: {full_name}")
158 |             
159 |             # Get methods
160 |             methods_query = """
161 |             MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
162 |             WHERE c.name = $class_name OR c.full_name = $class_name
163 |             RETURN m.name as name, m.params_list as params_list, m.params_detailed as params_detailed, m.return_type as return_type
164 |             ORDER BY m.name
165 |             """
166 |             result = await session.run(methods_query, class_name=class_name)
167 |             
168 |             methods = []
169 |             async for record in result:
170 |                 methods.append({
171 |                     'name': record['name'],
172 |                     'params_list': record['params_list'] or [],
173 |                     'params_detailed': record['params_detailed'] or [],
174 |                     'return_type': record['return_type'] or 'Any'
175 |                 })
176 |             
177 |             if methods:
178 |                 print(f"\n⚙️  Methods ({len(methods)}):")
179 |                 for i, method in enumerate(methods, 1):
180 |                     # Use detailed params if available, fall back to simple params
181 |                     params_to_show = method['params_detailed'] or method['params_list']
182 |                     params = ', '.join(params_to_show) if params_to_show else ''
183 |                     print(f"{i:2d}. {method['name']}({params}) -> {method['return_type']}")
184 |             else:
185 |                 print("\n⚙️  No methods found.")
186 |             
187 |             # Get attributes
188 |             attributes_query = """
189 |             MATCH (c:Class)-[:HAS_ATTRIBUTE]->(a:Attribute)
190 |             WHERE c.name = $class_name OR c.full_name = $class_name
191 |             RETURN a.name as name, a.type as type
192 |             ORDER BY a.name
193 |             """
194 |             result = await session.run(attributes_query, class_name=class_name)
195 |             
196 |             attributes = []
197 |             async for record in result:
198 |                 attributes.append({
199 |                     'name': record['name'],
200 |                     'type': record['type'] or 'Any'
201 |                 })
202 |             
203 |             if attributes:
204 |                 print(f"\n📋 Attributes ({len(attributes)}):")
205 |                 for i, attr in enumerate(attributes, 1):
206 |                     print(f"{i:2d}. {attr['name']}: {attr['type']}")
207 |             else:
208 |                 print("\n📋 No attributes found.")
209 |         
210 |         return {'methods': methods, 'attributes': attributes}
211 |     
212 |     async def search_method(self, method_name: str, class_name: str = None):
213 |         """Search for methods by name"""
214 |         title = f"Method '{method_name}'"
215 |         if class_name:
216 |             title += f" in class '{class_name}'"
217 |         
218 |         print(f"\n🔍 Searching for {title}:")
219 |         print("=" * 60)
220 |         
221 |         async with self.driver.session() as session:
222 |             if class_name:
223 |                 query = """
224 |                 MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
225 |                 WHERE (c.name = $class_name OR c.full_name = $class_name)
226 |                   AND m.name = $method_name
227 |                 RETURN c.name as class_name, c.full_name as class_full_name,
228 |                        m.name as method_name, m.params_list as params_list, 
229 |                        m.return_type as return_type, m.args as args
230 |                 """
231 |                 result = await session.run(query, class_name=class_name, method_name=method_name)
232 |             else:
233 |                 query = """
234 |                 MATCH (c:Class)-[:HAS_METHOD]->(m:Method)
235 |                 WHERE m.name = $method_name
236 |                 RETURN c.name as class_name, c.full_name as class_full_name,
237 |                        m.name as method_name, m.params_list as params_list, 
238 |                        m.return_type as return_type, m.args as args
239 |                 ORDER BY c.name
240 |                 """
241 |                 result = await session.run(query, method_name=method_name)
242 |             
243 |             methods = []
244 |             async for record in result:
245 |                 methods.append({
246 |                     'class_name': record['class_name'],
247 |                     'class_full_name': record['class_full_name'],
248 |                     'method_name': record['method_name'],
249 |                     'params_list': record['params_list'] or [],
250 |                     'return_type': record['return_type'] or 'Any',
251 |                     'args': record['args'] or []
252 |                 })
253 |             
254 |             if methods:
255 |                 for i, method in enumerate(methods, 1):
256 |                     params = ', '.join(method['params_list']) if method['params_list'] else ''
257 |                     print(f"{i}. {method['class_full_name']}.{method['method_name']}({params}) -> {method['return_type']}")
258 |                     if method['args']:
259 |                         print(f"   Legacy args: {method['args']}")
260 |             else:
261 |                 print(f"❌ Method '{method_name}' not found.")
262 |         
263 |         return methods
264 |     
265 |     async def run_custom_query(self, query: str):
266 |         """Run a custom Cypher query"""
267 |         print(f"\n🔍 Running Custom Query:")
268 |         print("=" * 60)
269 |         print(f"Query: {query}")
270 |         print("-" * 60)
271 |         
272 |         async with self.driver.session() as session:
273 |             try:
274 |                 result = await session.run(query)
275 |                 
276 |                 records = []
277 |                 async for record in result:
278 |                     records.append(dict(record))
279 |                 
280 |                 if records:
281 |                     for i, record in enumerate(records, 1):
282 |                         print(f"{i:2d}. {record}")
283 |                         if i >= 20:  # Limit output
284 |                             print(f"... and {len(records) - 20} more records")
285 |                             break
286 |                 else:
287 |                     print("No results found.")
288 |                 
289 |                 return records
290 |                 
291 |             except Exception as e:
292 |                 print(f"❌ Query error: {str(e)}")
293 |                 return None
294 | 
295 | 
296 | async def interactive_mode(querier: KnowledgeGraphQuerier):
297 |     """Interactive exploration mode"""
298 |     print("\n🚀 Welcome to Knowledge Graph Explorer!")
299 |     print("Available commands:")
300 |     print("  repos          - List all repositories")
301 |     print("  explore <repo> - Explore a specific repository") 
302 |     print("  classes [repo] - List classes (optionally in specific repo)")
303 |     print("  class <name>   - Explore a specific class")
304 |     print("  method <name> [class] - Search for method")
305 |     print("  query <cypher> - Run custom Cypher query")
306 |     print("  quit           - Exit")
307 |     print()
308 |     
309 |     while True:
310 |         try:
311 |             command = input("🔍 > ").strip()
312 |             
313 |             if not command:
314 |                 continue
315 |             elif command == "quit":
316 |                 break
317 |             elif command == "repos":
318 |                 await querier.list_repositories()
319 |             elif command.startswith("explore "):
320 |                 repo_name = command[8:].strip()
321 |                 await querier.explore_repository(repo_name)
322 |             elif command == "classes":
323 |                 await querier.list_classes()
324 |             elif command.startswith("classes "):
325 |                 repo_name = command[8:].strip()
326 |                 await querier.list_classes(repo_name)
327 |             elif command.startswith("class "):
328 |                 class_name = command[6:].strip()
329 |                 await querier.explore_class(class_name)
330 |             elif command.startswith("method "):
331 |                 parts = command[7:].strip().split()
332 |                 if len(parts) >= 2:
333 |                     await querier.search_method(parts[0], parts[1])
334 |                 else:
335 |                     await querier.search_method(parts[0])
336 |             elif command.startswith("query "):
337 |                 query = command[6:].strip()
338 |                 await querier.run_custom_query(query)
339 |             else:
340 |                 print("❌ Unknown command. Type 'quit' to exit.")
341 |                 
342 |         except KeyboardInterrupt:
343 |             print("\n👋 Goodbye!")
344 |             break
345 |         except Exception as e:
346 |             print(f"❌ Error: {str(e)}")
347 | 
348 | 
349 | async def main():
350 |     """Main function with CLI argument support"""
351 |     parser = argparse.ArgumentParser(description="Query the knowledge graph")
352 |     parser.add_argument('--repos', action='store_true', help='List repositories')
353 |     parser.add_argument('--classes', metavar='REPO', nargs='?', const='', help='List classes')
354 |     parser.add_argument('--explore', metavar='REPO', help='Explore repository')
355 |     parser.add_argument('--class', dest='class_name', metavar='NAME', help='Explore class')
356 |     parser.add_argument('--method', nargs='+', metavar=('NAME', 'CLASS'), help='Search method')
357 |     parser.add_argument('--query', metavar='CYPHER', help='Run custom query')
358 |     parser.add_argument('--interactive', action='store_true', help='Interactive mode')
359 |     
360 |     args = parser.parse_args()
361 |     
362 |     # Load environment
363 |     load_dotenv()
364 |     neo4j_uri = os.environ.get('NEO4J_URI', 'bolt://localhost:7687')
365 |     neo4j_user = os.environ.get('NEO4J_USER', 'neo4j')
366 |     neo4j_password = os.environ.get('NEO4J_PASSWORD', 'password')
367 |     
368 |     querier = KnowledgeGraphQuerier(neo4j_uri, neo4j_user, neo4j_password)
369 |     
370 |     try:
371 |         await querier.initialize()
372 |         
373 |         # Execute commands based on arguments
374 |         if args.repos:
375 |             await querier.list_repositories()
376 |         elif args.classes is not None:
377 |             await querier.list_classes(args.classes if args.classes else None)
378 |         elif args.explore:
379 |             await querier.explore_repository(args.explore)
380 |         elif args.class_name:
381 |             await querier.explore_class(args.class_name)
382 |         elif args.method:
383 |             if len(args.method) >= 2:
384 |                 await querier.search_method(args.method[0], args.method[1])
385 |             else:
386 |                 await querier.search_method(args.method[0])
387 |         elif args.query:
388 |             await querier.run_custom_query(args.query)
389 |         elif args.interactive or len(sys.argv) == 1:
390 |             await interactive_mode(querier)
391 |         else:
392 |             parser.print_help()
393 |     
394 |     finally:
395 |         await querier.close()
396 | 
397 | 
398 | if __name__ == "__main__":
399 |     import sys
400 |     asyncio.run(main())
```
Page 3/7FirstPrevNextLast