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())
```