diff --git a/community/server/pom.xml b/community/server/pom.xml
index 986ae75d2e23e..396a060c58985 100644
--- a/community/server/pom.xml
+++ b/community/server/pom.xml
@@ -507,52 +507,6 @@
-
- maven-antrun-plugin
- 1.7
-
-
-
- generate-source-based-documentation
- process-classes
-
-
-
-
-
-
-
-
- run
-
-
-
-
-
- ant-contrib
- ant-contrib
- 1.0b3
-
-
- ant
- ant
-
-
-
-
- org.apache.ant
- ant
- 1.8.2
-
-
- org.apache.ant
- ant-apache-regexp
- 1.8.2
-
-
-
-
diff --git a/community/server/src/docs/dev/images/jconsole_coordinator.png b/community/server/src/docs/dev/images/jconsole_coordinator.png
deleted file mode 100644
index bbd0044242556..0000000000000
Binary files a/community/server/src/docs/dev/images/jconsole_coordinator.png and /dev/null differ
diff --git a/community/server/src/docs/ops/powershell.asciidoc b/community/server/src/docs/ops/powershell.asciidoc
deleted file mode 100755
index e751010a8fa1b..0000000000000
--- a/community/server/src/docs/ops/powershell.asciidoc
+++ /dev/null
@@ -1,126 +0,0 @@
-[[powershell]]
-= Windows PowerShell module
-
-The Neo4j PowerShell module allows administrators to:
-
-* install, start and stop Neo4j Windows® Services
-* and start tools, such as `Neo4j Shell` and `Neo4j Import`.
-
-The PowerShell module is installed as part of the http://neo4j.com/download/[ZIP file] distributions of Neo4j.
-
-[[powershell-requirements]]
-== System Requirements
-
-* Requires PowerShell v2.0 or above.
-* Supported on either 32 or 64 bit operating systems.
-
-[[powershell-windows]]
-== Managing Neo4j on Windows
-
-On Windows it is sometimes necessary to _Unblock_ a downloaded zip file before you can import its contents as a module. If you right-click on the zip file and choose "Properties" you will get a dialog. Bottom-right on that dialog you will find an "Unblock" button. Click that. Then you should be able to import the module.
-
-Running scripts has to be enabled on the system.
-This can for example be achieved by executing the following from an elevated PowerShell prompt:
-[source,powershell]
-----
-Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
-----
-For more information see https://technet.microsoft.com/en-us/library/hh847748.aspx[About execution policies].
-
-The powershell module will display a warning if it detects that you do not have administrative rights.
-
-[[powershell-module-import]]
-== How do I import the module?
-
-The module file is located in the _bin_ directory of your Neo4j installation, i.e. where you unzipped the downloaded file.
-For example, if Neo4j was installed in _C:\Neo4j_ then the module would be imported like this:
-
-[source,powershell]
-----
-Import-Module C:\Neo4j\bin\Neo4j-Management.psd1
-----
-
-This will add the module to the current session.
-
-Once the module has been imported you can start an interactive console version of a Neo4j Server like this:
-
-[source,powershell]
-----
-Invoke-Neo4j console
-----
-
-To stop the server, issue `Ctrl-C` in the console window that was created by the command.
-
-[[powershell-help]]
-== How do I get help about the module?
-
-Once the module is imported you can query the available commands like this:
-
-[source,powershell]
-----
-Get-Command -Module Neo4j-Management
-----
-
-The output should be similar to the following:
-
-[source]
-----
-CommandType Name Version Source
------------ ---- ------- ------
-Function Invoke-Neo4j 3.0.0 Neo4j-Management
-Function Invoke-Neo4jAdmin 3.0.0 Neo4j-Management
-Function Invoke-Neo4jBackup 3.0.0 Neo4j-Management
-Function Invoke-Neo4jImport 3.0.0 Neo4j-Management
-Function Invoke-Neo4jShell 3.0.0 Neo4j-Management
-----
-
-The module also supports the standard PowerShell help commands.
-
-[source,powershell]
-----
-Get-Help Invoke-Neo4j
-----
-
-To see examples for a command, do like this:
-
-[source,powershell]
-----
-Get-Help Invoke-Neo4j -examples
-----
-
-[[powershell-examples]]
-== Example usage
-
-* List of available commands:
-+
-[source,powershell]
-----
-Invoke-Neo4j
-----
-
-* Current status of the Neo4j service:
-+
-[source,powershell]
-----
-Invoke-Neo4j status
-----
-
-* Install the service with verbose output:
-+
-[source,powershell]
-----
-Invoke-Neo4j install-service -Verbose
-----
-
-* Available commands for administrative tasks:
-+
-[source,powershell]
-----
-Invoke-Neo4jAdmin
-----
-
-[[powershell-common-parameters]]
-== Common PowerShell parameters
-
-The module commands support the common PowerShell parameter of `Verbose`.
-
diff --git a/community/server/src/docs/ops/security.asciidoc b/community/server/src/docs/ops/security.asciidoc
deleted file mode 100644
index 9a2118866a321..0000000000000
--- a/community/server/src/docs/ops/security.asciidoc
+++ /dev/null
@@ -1,163 +0,0 @@
-[[security-server]]
-= Securing Neo4j Server
-
-== Secure the port and remote client connection accepts ==
-
-By default, the Neo4j Server is bundled with a Web server that binds to host +localhost+ on port +7474+, answering only requests from the local machine.
-
-This is configured in <>:
-
-[source,properties]
-----
-# Let the webserver only listen on the specified IP. Default is localhost (only
-# accept local connections). Uncomment to allow any connection.
-dbms.connector.http.type=HTTP
-dbms.connector.http.enabled=true
-#dbms.connector.http.address=0.0.0.0:7474
-----
-
-If you want the server to listen to external hosts, configure the Web server in _neo4j.conf_ by setting the property +dbms.connector.http.address=0.0.0.0:7474+ which will cause the server to bind to all available network interfaces.
-Note that firewalls et cetera have to be configured accordingly as well.
-
-[[security-server-auth]]
-== Server authentication and authorization ==
-
-Neo4j requires clients to supply authentication credentials when accessing the REST API.
-Without valid credentials, access to the database will be forbidden.
-
-The authentication and authorization data is stored under _data/dbms/auth_.
-If necessary, this file can be copied over to other neo4j instances to ensure they share the same username/password (see <>).
-
-Please refer to <> for additional details.
-When accessing Neo4j over unsecured networks, make sure HTTPS is configured and used for access (see <>).
-
-If necessary, authentication may be disabled.
-This will allow any client to access the database without supplying authentication credentials.
-
-[source,properties]
-----
-# Disable authorization
-dbms.security.auth_enabled=false
-----
-
-[WARNING]
-Disabling authentication is not recommended, and should only be done if the operator has a good understanding of their network security, including protection against http://en.wikipedia.org/wiki/Cross-site_scripting[cross-site scripting (XSS)] attacks via web browsers.
-Developers should not disable authentication if they have a local installation using the default listening ports.
-
-[[security-server-https]]
-== HTTPS support ==
-
-The Neo4j server includes built in support for SSL encrypted communication over HTTPS.
-The first time the server starts, it automatically generates a self-signed SSL certificate and a private key.
-Because the certificate is self signed, it is not safe to rely on for production use, instead, you should provide your own key and certificate for the server to use.
-
-[CAUTION]
-Using auto-generation of self-signed SSL certificates will not work if the Neo4j server has been configured with multiple connectors that bind to different IP addresses.
-If you need to use multiple IP addresses, please configure certificates manually and use multi-host or wildcard certificates instead.
-
-To provide your own key and certificate put them in the <> directory.
-The files must be named _neo4j.key_ and _neo4j.cert_.
-The location of the directory can be configured by setting _dbms.directories.certificates_ in <>.
-
-[source,properties]
-----
-# Certificates location (auto generated if the file does not exist)
-dbms.directories.certificates=certificates
-
-----
-
-Note that the key should be unencrypted.
-Make sure you set correct permissions on the private key, so that only the Neo4j server user can read/write it.
-
-Neo4j also supports chained SSL certificates.
-This requires to have all certificates in PEM format combined in one file and the private key needs to be in DER format.
-
-You can set what port the HTTPS connector should bind to in the same configuration file, as well as turn HTTPS on or off:
-
-[source,properties]
-----
-dbms.connector.https.type=HTTP
-dbms.connector.https.enabled=true
-dbms.connector.https.encryption=TLS
-dbms.connector.https.address=localhost:7473
-----
-
-== Arbitrary code execution ==
-
-[IMPORTANT]
-The Neo4j server exposes remote scripting functionality by default that allow full access to the underlying system.
-Exposing your server without implementing a security layer presents a substantial security vulnerability.
-
-By default, the Neo4j Server comes with some places where arbitrary code code execution can happen. These are the <> REST endpoints.
-To secure these, either disable them completely by removing offending plugins from the server classpath, or secure access to these URLs through proxies or Authorization Rules.
-Also, the Java Security Manager, see http://docs.oracle.com/javase/7/docs/technotes/guides/security/index.html, can be used to secure parts of the codebase.
-
-== Server authorization rules ==
-
-Administrators may require more fine-grained security policies in addition to the basic authorization and/or IP-level restrictions on the Web server.
-Neo4j server supports administrators in allowing or disallowing access the specific aspects of the database based on credentials that users or applications provide.
-
-To facilitate domain-specific authorization policies in Neo4j Server, security rules can be implemented and registered with the server.
-This makes scenarios like user and role based security and authentication against external lookup services possible.
-See +org.neo4j.server.rest.security.SecurityRule+ in the javadocs downloadable from http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.neo4j.app%22%20AND%20a%3A%22neo4j-server%22[Maven Central (org.neo4j.app:neo4j-server)].
-
-[CAUTION]
-The use of Server Authorization Rules may interact unexpectedly with the built-in authentication and authorization (see <>), if enabled.
-
-include::enforcing-server-authorization-rules.asciidoc[]
-
-include::using-wildcards-to-target-security-rules.asciidoc[]
-
-include::using-complex-wildcards-to-target-security-rules.asciidoc[]
-
-
-== Using a proxy ==
-
-Although the Neo4j server has a number of security features built-in (see the above chapters), for sensitive deployments it is often sensible to front against the outside world it with a proxy like Apache `mod_proxy` footnote:[http://httpd.apache.org/docs/2.2/mod/mod_proxy.html].
-
-This provides a number of advantages:
-
-* Control access to the Neo4j server to specific IP addresses, URL patterns and IP ranges. This can be used to make for instance only the '/db/data' namespace accessible to non-local clients, while the '/db/admin' URLs only respond to a specific IP address.
-+
-[source]
----------------
-
- Order Deny,Allow
- Deny from all
- Allow from 192.168.0
-
----------------
-+
-While it is possible to develop plugins using Neo4j's `SecurityRule` (see above), operations professionals would often prefer to configure proxy servers such as Apache.
-However, it should be noted that in cases where both approaches are being used, they will work harmoniously provided that the behavior is consistent across proxy server and `SecurityRule` plugins.
-
-* Run Neo4j Server as a non-root user on a Linux/Unix system on a port < 1000 (e.g. port 80) using
-+
-[source]
----------------
-ProxyPass /neo4jdb/data http://localhost:7474/db/data
-ProxyPassReverse /neo4jdb/data http://localhost:7474/db/data
----------------
-
-* Simple load balancing in a clustered environment to load-balance read load using the Apache `mod_proxy_balancer` footnote:[http://httpd.apache.org/docs/2.2/mod/mod_proxy_balancer.html] plugin
-+
-[source]
---------------
-
-BalancerMember http://192.168.1.50:80
-BalancerMember http://192.168.1.51:80
-
-ProxyPass /test balancer://mycluster
---------------
-
-== LOAD CSV
-
-The Cypher `LOAD CSV` clause can load files from the filesystem, and its default configuration allows any file on the system to be read using a `file:///` URL.
-This presents a security vulnerability in production environments where database users should not otherwise have access to files on the system.
-For production deployments, configure the <> setting, which will make all files identified in a `file:///` URL relative to the specified directory, similarly to how a unix chroot works.
-Alternatively, set the <> setting to false, which disables the use of `file:///` URLs entirely.
-Further information can be found in <>.
-
-== Neo4j Web Interface Security
-
-For configuration settings to consider in order to get the level of security you want to achieve, see <>.
diff --git a/community/server/src/docs/ops/server-debugging.asciidoc b/community/server/src/docs/ops/server-debugging.asciidoc
deleted file mode 100644
index 896d3eb11e3cd..0000000000000
--- a/community/server/src/docs/ops/server-debugging.asciidoc
+++ /dev/null
@@ -1,16 +0,0 @@
-[[server-debugging]]
-= Setup for remote debugging
-
-In order to configure the Neo4j server for remote debugging sessions, the Java debugging parameters need to be passed to the Java process through the configuration.
-They live in the _conf/neo4j-wrapper.properties_ file.
-
-In order to specify the parameters, add a line for the additional Java arguments like this:
-
-[source,properties]
-----
-dbms.jvm.additional=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
-----
-
-This configuration will start a Neo4j server ready for remote debugging attachement at localhost and port `5005`.
-Use these parameters to attach to the process from Eclipse, IntelliJ or your remote debugger of choice after starting the server.
-
diff --git a/community/server/src/docs/ops/server-installation.asciidoc b/community/server/src/docs/ops/server-installation.asciidoc
deleted file mode 100644
index 08201749ce59e..0000000000000
--- a/community/server/src/docs/ops/server-installation.asciidoc
+++ /dev/null
@@ -1,152 +0,0 @@
-[[server-installation]]
-= Server Installation
-
-== Deployment Scenarios ==
-
-As a developer, you may wish to download Neo4j and run it locally on your desktop computer.
-We recommend this as an easy way to discover Neo4j.
-
-* For Windows, see <>.
-* For Unix/Linux, see <>.
-* For OSX, see <>.
-
-As a systems administrator, you may wish to install Neo4j using a packaging system so you can ensure that a cluster of machines have identical installs.
-See <> for more information on this.
-
-For information on High Availability, please refer to <>.
-
-== Prerequisites ==
-
-With the exception of our Windows and Mac Installers, you'll need a Java Virtual Machine installed on your computer.
-We recommend that you install http://openjdk.java.net/[OpenJDK 8],
-http://www.oracle.com/technetwork/java/javase/downloads/index.html[Oracle Java 8] or
-http://www.ibm.com/developerworks/java/jdk/[IBM Java 8] (POWER8 only).
-
-[[server-permissions]]
-== Setting Proper File Permissions ==
-
-When installing Neo4j Server, keep in mind that the _bin/neo4j_ executable will need to be run by some OS system user, and that user will need write permissions to some files/directories.
-This goes specifically for the _data_ directory.
-That user will also need execute permissions on other files, such as those in the _bin_ directory.
-
-It is recommended to either choose or create a user who will own and manage the Neo4j Server.
-This user should own the entire Neo4j directory, so make sure to untar/unzip it as this user and not with `sudo` (UNIX/Linux/OSx) etc.
-
-If the _data_ directory is not writable by the user Neo4j won't be able to write anything either to the store.
-As a result any logs would be appended to _neo4j.log_.
-The following error message would indicate a possible permissions issue: `Write transactions to database disabled`.
-
-[[windows-install]]
-== Windows ==
-
-[[windows-installer]]
-=== Windows Installer ===
-
-1. Download the version that you want from http://neo4j.com/download/.
- * Select the appropriate version and architecture for your platform.
-2. Double-click the downloaded installer file.
-3. Follow the prompts.
-
-[NOTE]
-The installer will prompt to be granted Administrator privileges.
-Newer versions of Windows come with a SmartScreen feature that may prevent the installer from running -- you can make it run anyway by clicking "More info" on the "Windows protected your PC" screen.
-
-[TIP]
-If you install Neo4j using the windows installer and you already have an existing instance of Neo4j the installer will select a new install directory by default.
-If you specify the same directory it will ask if you want to upgrade.
-This should proceed without issue although some users have reported a `JRE is damaged` error.
-If you see this error simply install Neo4j into a different location.
-
-[[windows-console]]
-=== Windows Console Application ===
-1. Download the latest release from http://neo4j.com/download/.
- * Select the appropriate Zip distribution.
-2. Right-click the downloaded file, click Extract All.
-3. Change directory to top-level extracted directory.
- * Run `bin\neo4j console`
-4. Stop the server by typing Ctrl-C in the console.
-
-[NOTE]
-Some users have reported problems on Windows when using the ZoneAlarm firewall.
-If you are having problems getting large responses from the server, or if the web interface does not work, try disabling ZoneAlarm.
-Contact ZoneAlarm support to get information on how to resolve this.
-
-=== Windows service ===
-
-Neo4j can also be run as a Windows service.
-Install the service with `bin\neo4j install-service` and start it with `bin\neo4j start`.
-Other commands available are `stop`, `restart`, `status` and `uninstall-service`.
-
-[[linux-install]]
-== Linux ==
-
-[[linux-packages]]
-=== Linux Packages ===
-
-* For Debian packages, see the instructions at http://debian.neo4j.org/.
-
-After installation you may have to do some platform specific configuration and performance tuning.
-For that, refer to <>.
-
-[[unix-console]]
-=== Unix Console Application ===
-
-1. Download the latest release from http://neo4j.com/download/.
- * Select the appropriate tar.gz distribution for your platform.
-2. Extract the contents of the archive, using: `tar -xf `
- * Refer to the top-level extracted directory as: +NEO4J_HOME+
-3. Change directory to: `$NEO4J_HOME`
- * Run: `./bin/neo4j console`
-4. Stop the server by typing Ctrl-C in the console.
-
-=== Linux Service ===
-
-The `neo4j` command can also be used with `start`, `stop`, `restart` or `status` instead of `console`.
-By using these actions, you can create a Neo4j service.
-See the <> for further details.
-
-[CAUTION]
-This approach to running Neo4j as a service is deprecated.
-We strongly advise you to run Neo4j from a package where feasible.
-
-You can build your own `init.d` script.
-See for instance the Linux Standard Base specification on http://refspecs.linuxfoundation.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/tocsysinit.html[system initialization], or one of the many https://gist.github.com/chrisvest/7673244[samples] and http://www.linux.com/learn/tutorials/442412-managing-linux-daemons-with-init-scripts[tutorials].
-
-[[osx-install]]
-== Mac OSX ==
-
-=== Mac OSX Installer ===
-
-1. Download the _.dmg_ installer that you want from http://neo4j.com/download/.
-2. Click the downloaded installer file.
-3. Drag the Neo4j icon into the Applications folder.
-
-[TIP]
-If you install Neo4j using the Mac installer and already have an existing instance of Neo4j the installer will ensure that both the old and new versions can co-exist on your system.
-
-=== Running Neo4j from the Terminal ===
-
-The server can be started in the background from the terminal with the command `neo4j start`, and then stopped again with `neo4j stop`.
-The server can also be started in the foreground with `neo4j console` -- then its log output will be printed to the terminal.
-
-The `neo4j-shell` command can be used to interact with Neo4j from the command line using Cypher. It will automatically connect to any
-server that is running on localhost with the default port, otherwise it will show a help message. You can alternatively start the
-shell with an embedded Neo4j instance, by using the `-path path/to/data` argument -- note that only a single instance of Neo4j
-can access the database files at a time.
-
-=== OSX Service ===
-
-Use the standard OSX system tools to create a service based on the `neo4j` command.
-
-=== A note on Java on OS X Mavericks ===
-
-Unlike previous versions, OS X Mavericks does not come with Java pre-installed. You might encounter that the first time you run Neo4j, where OS X will trigger a popup offering you to install Java SE 6.
-
-Java SE 6 or 7 is incompatible with Neo4j {neo4j-version}, so we strongly advise you to skip installing Java SE 6 or 7 if you have no other uses for it. Instead, for Neo4j {neo4j-version} we recommend you install Java SE 8 from Oracle (http://www.oracle.com/technetwork/java/javase/downloads/index.html) as that is what we support for production use.
-
-== Multiple Server instances on one machine ==
-
-Neo4j can be set up to run as several instances on one machine, providing for instance several databases for development.
-
-For how to set this up, see <>.
-Just use the Neo4j edition of your choice, follow the guide and remember to not set the servers to run in HA mode.
diff --git a/community/server/src/docs/ops/server-performance.asciidoc b/community/server/src/docs/ops/server-performance.asciidoc
deleted file mode 100644
index 6c60c1ca2c873..0000000000000
--- a/community/server/src/docs/ops/server-performance.asciidoc
+++ /dev/null
@@ -1,31 +0,0 @@
-[[server-performance]]
-Server Performance Tuning
-=========================
-
-At the heart of the Neo4j server is a regular Neo4j storage engine instance.
-That engine can be tuned in the same way as the other embedded configurations, using the same file format.
-
-Specifying Neo4j tuning properties
-----------------------------------
-
-In the server distribution <> is the main configuration file for Neo4j.
-On restarting the server the tuning enhancements specified in this file will be loaded and configured into the underlying database engine.
-
-Specifying JVM tuning properties
---------------------------------
-
-Tuning the standalone server is achieved by editing the _neo4j-wrapper.conf_ file in the +conf+ directory of +NEO4J_HOME+.
-
-Edit the following properties:
-
-.neo4j-wrapper.conf JVM tuning properties
-[options="header", cols=">.
-
diff --git a/community/server/src/test/java/org/neo4j/server/BatchOperationHeaderIT.java b/community/server/src/test/java/org/neo4j/server/BatchOperationHeaderIT.java
new file mode 100644
index 0000000000000..e7d24bf72b050
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/BatchOperationHeaderIT.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.PrettyJSON;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.dummy.web.service.DummyThirdPartyWebService.DUMMY_WEB_SERVICE_MOUNT_POINT;
+import static org.junit.Assert.assertEquals;
+
+import static org.neo4j.server.helpers.CommunityServerBuilder.server;
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToList;
+
+public class BatchOperationHeaderIT extends ExclusiveServerTestBase
+{
+ private NeoServer server;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ @Before
+ public void cleanTheDatabase() throws IOException
+ {
+ server = server().withThirdPartyJaxRsPackage( "org.dummy.web.service",
+ DUMMY_WEB_SERVICE_MOUNT_POINT ).usingDataDir( folder.getRoot().getAbsolutePath() ).build();
+ server.start();
+ }
+
+ @After
+ public void stopServer()
+ {
+ if ( server != null )
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void shouldPassHeaders() throws Exception
+ {
+ String jsonData = new PrettyJSON()
+ .array()
+ .object()
+ .key( "method" ).value( "GET" )
+ .key( "to" ).value( "../.." + DUMMY_WEB_SERVICE_MOUNT_POINT + "/needs-auth-header" )
+ .key( "body" ).object().endObject()
+ .endObject()
+ .endArray()
+ .toString();
+
+ JaxRsResponse response = new RestRequest( null, "user", "pass" )
+ .post( "http://localhost:7474/db/data/batch", jsonData );
+
+ assertEquals( 200, response.getStatus() );
+
+ final List> responseData = jsonToList( response.getEntity() );
+
+ Map res = (Map) responseData.get( 0 ).get( "body" );
+
+ /*
+ * {
+ * Accept=[application/json],
+ * Content-Type=[application/json],
+ * Authorization=[Basic dXNlcjpwYXNz],
+ * User-Agent=[Java/1.6.0_27] <-- ignore that, it changes often
+ * Host=[localhost:7474],
+ * Connection=[keep-alive],
+ * Content-Length=[86]
+ * }
+ */
+ assertEquals( "Basic dXNlcjpwYXNz", res.get( "Authorization" ) );
+ assertEquals( "application/json", res.get( "Accept" ) );
+ assertEquals( "application/json", res.get( "Content-Type" ) );
+ assertEquals( "localhost:7474", res.get( "Host" ) );
+ assertEquals( "keep-alive", res.get( "Connection" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerDefaultPortAndHostnameIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerDefaultPortAndHostnameIT.java
new file mode 100644
index 0000000000000..efde711dbd40e
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerDefaultPortAndHostnameIT.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.AbstractRestFunctionalTestBase;
+import org.neo4j.server.rest.JaxRsResponse;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class NeoServerDefaultPortAndHostnameIT extends AbstractRestFunctionalTestBase
+{
+ @Test
+ public void shouldDefaultToSensiblePortIfNoneSpecifiedInConfig() throws Exception
+ {
+ FunctionalTestHelper functionalTestHelper = new FunctionalTestHelper( server() );
+
+ JaxRsResponse response = functionalTestHelper.get( functionalTestHelper.managementUri() );
+
+ assertThat( response.getStatus(), is( 200 ) );
+ }
+
+ @Test
+ public void shouldDefaultToLocalhostOfNoneSpecifiedInConfig() throws Exception
+ {
+ assertThat( server().baseUri().getHost(), is( "localhost" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerIT.java
new file mode 100644
index 0000000000000..2f798449c5943
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerIT.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import org.junit.Test;
+
+import org.neo4j.server.rest.AbstractRestFunctionalTestBase;
+import org.neo4j.test.server.HTTP;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+public class NeoServerIT extends AbstractRestFunctionalTestBase
+{
+ @Test
+ public void whenServerIsStartedItshouldStartASingleDatabase() throws Exception
+ {
+ assertNotNull( server().getDatabase() );
+ }
+
+ @Test
+ public void shouldRedirectRootToBrowser() throws Exception
+ {
+ assertFalse( server().baseUri()
+ .toString()
+ .contains( "browser" ) );
+
+ HTTP.Response res = HTTP.withHeaders( HttpHeaders.ACCEPT, MediaType.TEXT_HTML ).GET( server().baseUri().toString() );
+ assertThat( res.header( "Location" ), containsString( "browser") );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerJAXRSIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerJAXRSIT.java
new file mode 100644
index 0000000000000..9c0ae697d4932
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerJAXRSIT.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.net.URI;
+
+import org.dummy.web.service.DummyThirdPartyWebService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.RelationshipType;
+import org.neo4j.kernel.internal.GraphDatabaseAPI;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.helpers.ServerHelper;
+import org.neo4j.server.helpers.Transactor;
+import org.neo4j.server.helpers.UnitOfWork;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.junit.Assert.assertEquals;
+
+import static org.neo4j.server.helpers.FunctionalTestHelper.CLIENT;
+
+public class NeoServerJAXRSIT extends ExclusiveServerTestBase
+{
+ private NeoServer server;
+
+ @Before
+ public void cleanTheDatabase()
+ {
+ ServerHelper.cleanTheDatabase( server );
+ }
+
+ @After
+ public void stopServer()
+ {
+ if ( server != null )
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void shouldMakeJAXRSClassesAvailableViaHTTP() throws Exception
+ {
+ CommunityServerBuilder builder = CommunityServerBuilder.server();
+ server = ServerHelper.createNonPersistentServer( builder );
+ FunctionalTestHelper functionalTestHelper = new FunctionalTestHelper( server );
+
+ JaxRsResponse response = new RestRequest().get( functionalTestHelper.managementUri() );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldLoadThirdPartyJaxRsClasses() throws Exception
+ {
+ server = CommunityServerBuilder.server()
+ .withThirdPartyJaxRsPackage( "org.dummy.web.service",
+ DummyThirdPartyWebService.DUMMY_WEB_SERVICE_MOUNT_POINT )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+
+ URI thirdPartyServiceUri = new URI( server.baseUri()
+ .toString() + DummyThirdPartyWebService.DUMMY_WEB_SERVICE_MOUNT_POINT ).normalize();
+ String response = CLIENT.resource( thirdPartyServiceUri.toString() )
+ .get( String.class );
+ assertEquals( "hello", response );
+
+ // Assert that extensions gets initialized
+ int nodesCreated = createSimpleDatabase( server.getDatabase().getGraph() );
+ thirdPartyServiceUri = new URI( server.baseUri()
+ .toString() + DummyThirdPartyWebService.DUMMY_WEB_SERVICE_MOUNT_POINT + "/inject-test" ).normalize();
+ response = CLIENT.resource( thirdPartyServiceUri.toString() )
+ .get( String.class );
+ assertEquals( String.valueOf( nodesCreated ), response );
+ }
+
+ private int createSimpleDatabase( final GraphDatabaseAPI graph )
+ {
+ final int numberOfNodes = 10;
+ new Transactor( graph, new UnitOfWork()
+ {
+
+ @Override
+ public void doWork()
+ {
+ for ( int i = 0; i < numberOfNodes; i++ )
+ {
+ graph.createNode();
+ }
+
+ for ( Node n1 : graph.getAllNodes() )
+ {
+ for ( Node n2 : graph.getAllNodes() )
+ {
+ if ( n1.equals( n2 ) )
+ {
+ continue;
+ }
+
+ n1.createRelationshipTo( n2, RelationshipType.withName( "REL" ) );
+ }
+ }
+ }
+ } ).execute();
+
+ return numberOfNodes;
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerPortConflictIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerPortConflictIT.java
new file mode 100644
index 0000000000000..f019de42baccb
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerPortConflictIT.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+
+import org.junit.Test;
+
+import org.neo4j.helpers.HostnamePort;
+import org.neo4j.logging.AssertableLogProvider;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static java.lang.String.format;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+public class NeoServerPortConflictIT extends ExclusiveServerTestBase
+{
+ @Test
+ public void shouldComplainIfServerPortIsAlreadyTaken() throws IOException, InterruptedException
+ {
+ HostnamePort contestedAddress = new HostnamePort( "localhost", 9999 );
+ try ( ServerSocket ignored = new ServerSocket(
+ contestedAddress.getPort(), 0, InetAddress.getByName( contestedAddress.getHost() ) ) )
+ {
+ AssertableLogProvider logProvider = new AssertableLogProvider();
+ CommunityNeoServer server = CommunityServerBuilder.server( logProvider )
+ .onAddress( contestedAddress )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ try
+ {
+ server.start();
+
+ fail( "Should have reported failure to start" );
+ }
+ catch ( ServerStartupException e )
+ {
+ assertThat( e.getMessage(), containsString( "Starting Neo4j failed" ) );
+ }
+
+ logProvider.assertAtLeastOnce(
+ AssertableLogProvider.inLog( containsString( "CommunityNeoServer" ) ).error(
+ "Failed to start Neo4j on %s: %s",
+ contestedAddress,
+ format( "Address %s is already in use, cannot bind to it.", contestedAddress )
+ )
+ );
+ server.stop();
+ }
+ }
+
+ @Test
+ public void shouldComplainIfServerHTTPSPortIsAlreadyTaken() throws IOException, InterruptedException
+ {
+ HostnamePort unContestedAddress = new HostnamePort( "localhost", 8888 );
+ HostnamePort contestedAddress = new HostnamePort( "localhost", 9999 );
+ try ( ServerSocket ignored = new ServerSocket(
+ contestedAddress.getPort(), 0, InetAddress.getByName( contestedAddress.getHost() ) ) )
+ {
+ AssertableLogProvider logProvider = new AssertableLogProvider();
+ CommunityNeoServer server = CommunityServerBuilder.server( logProvider )
+ .onAddress( unContestedAddress )
+ .onHttpsAddress( contestedAddress )
+ .withHttpsEnabled()
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ try
+ {
+ server.start();
+
+ fail( "Should have reported failure to start" );
+ }
+ catch ( ServerStartupException e )
+ {
+ assertThat( e.getMessage(), containsString( "Starting Neo4j failed" ) );
+ }
+
+ logProvider.assertAtLeastOnce(
+ AssertableLogProvider.inLog( containsString( "CommunityNeoServer" ) ).error(
+ "Failed to start Neo4j on %s: %s",
+ unContestedAddress,
+ format( "At least one of the addresses %s or %s is already in use, cannot bind to it.",
+ unContestedAddress, contestedAddress )
+ )
+ );
+ server.stop();
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerShutdownLoggingIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerShutdownLoggingIT.java
new file mode 100644
index 0000000000000..442dad4979330
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerShutdownLoggingIT.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.io.IOException;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.neo4j.logging.AssertableLogProvider;
+import org.neo4j.server.helpers.ServerHelper;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+public class NeoServerShutdownLoggingIT extends ExclusiveServerTestBase
+{
+ private AssertableLogProvider logProvider;
+ private NeoServer server;
+
+ @Before
+ public void setupServer() throws IOException
+ {
+ logProvider = new AssertableLogProvider();
+ server = ServerHelper.createNonPersistentServer( logProvider );
+ ServerHelper.cleanTheDatabase( server );
+ }
+
+ @After
+ public void shutdownTheServer()
+ {
+ if ( server != null )
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ public void shouldLogShutdown() throws Exception
+ {
+ server.stop();
+ logProvider.assertContainsMessageContaining( "Stopped." );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/NeoServerStartupLoggingIT.java b/community/server/src/test/java/org/neo4j/server/NeoServerStartupLoggingIT.java
new file mode 100644
index 0000000000000..2c4548ff4d7fc
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/NeoServerStartupLoggingIT.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import com.sun.jersey.api.client.Client;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.logging.FormattedLogProvider;
+import org.neo4j.server.helpers.ServerHelper;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class NeoServerStartupLoggingIT extends ExclusiveServerTestBase
+{
+ private static ByteArrayOutputStream out;
+ private static NeoServer server;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ out = new ByteArrayOutputStream();
+ server = ServerHelper.createNonPersistentServer( FormattedLogProvider.toOutputStream( out ) );
+ }
+
+ @Before
+ public void cleanTheDatabase()
+ {
+ ServerHelper.cleanTheDatabase( server );
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ server.stop();
+ }
+
+ @Test
+ public void shouldLogStartup() throws Exception
+ {
+ // Check the logs
+ assertThat( out.toString().length(), is( greaterThan( 0 ) ) );
+
+ // Check the server is alive
+ Client nonRedirectingClient = Client.create();
+ nonRedirectingClient.setFollowRedirects( false );
+ final JaxRsResponse response = new RestRequest(server.baseUri(), nonRedirectingClient).get();
+ assertThat( response.getStatus(), is( greaterThan( 199 ) ) );
+
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/ServerConfigIT.java b/community/server/src/test/java/org/neo4j/server/ServerConfigIT.java
new file mode 100644
index 0000000000000..610598372b148
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/ServerConfigIT.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.io.IOException;
+import javax.ws.rs.core.MediaType;
+
+import org.junit.After;
+import org.junit.Test;
+
+import org.neo4j.helpers.HostnamePort;
+import org.neo4j.server.configuration.ServerSettings;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.server.scripting.javascript.GlobalJavascriptInitializer;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import static org.neo4j.server.helpers.CommunityServerBuilder.server;
+import static org.neo4j.test.server.HTTP.POST;
+
+public class ServerConfigIT extends ExclusiveServerTestBase
+{
+ private CommunityNeoServer server;
+
+ @After
+ public void stopTheServer()
+ {
+ server.stop();
+ }
+
+ @Test
+ public void shouldPickUpAddressFromConfig() throws Exception
+ {
+ HostnamePort nonDefaultAddress = new HostnamePort( "0.0.0.0", 4321 );
+ server = server().onAddress( nonDefaultAddress )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+
+ assertEquals( nonDefaultAddress, server.getAddress() );
+
+ JaxRsResponse response = new RestRequest( server.baseUri() ).get();
+
+ assertThat( response.getStatus(), is( 200 ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldPickupRelativeUrisForMangementApiAndRestApi() throws IOException
+ {
+ String dataUri = "/a/different/data/uri/";
+ String managementUri = "/a/different/management/uri/";
+
+ server = server().withRelativeRestApiUriPath( dataUri )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .withRelativeManagementApiUriPath( managementUri )
+ .build();
+ server.start();
+
+ JaxRsResponse response = new RestRequest().get( "http://localhost:7474" + dataUri,
+ MediaType.TEXT_HTML_TYPE );
+ assertEquals( 200, response.getStatus() );
+
+ response = new RestRequest().get( "http://localhost:7474" + managementUri );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGenerateWADLWhenExplicitlyEnabledInConfig() throws IOException
+ {
+ server = server().withProperty( ServerSettings.wadl_enabled.name(), "true" )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ JaxRsResponse response = new RestRequest().get( "http://localhost:7474/application.wadl",
+ MediaType.WILDCARD_TYPE );
+
+ assertEquals( 200, response.getStatus() );
+ assertEquals( "application/vnd.sun.wadl+xml", response.getHeaders().get( "Content-Type" ).iterator().next() );
+ assertThat( response.getEntity(), containsString( "" ) );
+ }
+
+ @Test
+ public void shouldNotGenerateWADLWhenNotExplicitlyEnabledInConfig() throws IOException
+ {
+ server = server()
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ JaxRsResponse response = new RestRequest().get( "http://localhost:7474/application.wadl",
+ MediaType.WILDCARD_TYPE );
+
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Test
+ public void shouldNotGenerateWADLWhenExplicitlyDisabledInConfig() throws IOException
+ {
+ server = server().withProperty( ServerSettings.wadl_enabled.name(), "false" )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ JaxRsResponse response = new RestRequest().get( "http://localhost:7474/application.wadl",
+ MediaType.WILDCARD_TYPE );
+
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Test
+ public void shouldEnablConsoleServiceByDefault() throws IOException
+ {
+ // Given
+ server = server().usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() ).build();
+ server.start();
+
+ // When & then
+ assertEquals( 200, new RestRequest().get( "http://localhost:7474/db/manage/server/console" ).getStatus() );
+ }
+
+ @Test
+ public void shouldDisableConsoleServiceWhenAskedTo() throws IOException
+ {
+ // Given
+ server = server().withProperty( ServerSettings.console_module_enabled.name(), "false" )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+
+ // When & then
+ assertEquals( 404, new RestRequest().get( "http://localhost:7474/db/manage/server/console" ).getStatus() );
+ }
+
+ @Test
+ public void shouldHaveSandboxingEnabledByDefault() throws Exception
+ {
+ // Given
+ server = server()
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ String node = POST( server.baseUri().toASCIIString() + "db/data/node" ).location();
+
+ // When
+ JaxRsResponse response = new RestRequest().post( node + "/traverse/node", "{\n" +
+ " \"order\" : \"breadth_first\",\n" +
+ " \"return_filter\" : {\n" +
+ " \"body\" : \"position.getClass().getClassLoader()\",\n" +
+ " \"language\" : \"javascript\"\n" +
+ " },\n" +
+ " \"prune_evaluator\" : {\n" +
+ " \"body\" : \"position.getClass().getClassLoader()\",\n" +
+ " \"language\" : \"javascript\"\n" +
+ " },\n" +
+ " \"uniqueness\" : \"node_global\",\n" +
+ " \"relationships\" : [ {\n" +
+ " \"direction\" : \"all\",\n" +
+ " \"type\" : \"knows\"\n" +
+ " }, {\n" +
+ " \"direction\" : \"all\",\n" +
+ " \"type\" : \"loves\"\n" +
+ " } ],\n" +
+ " \"max_depth\" : 3\n" +
+ "}", MediaType.APPLICATION_JSON_TYPE );
+
+ // Then
+ assertEquals( 400, response.getStatus() );
+ }
+
+ /*
+ * We can't actually test that disabling sandboxing works, because of the set-once global nature of Rhino
+ * security. Instead, we test here that changing it triggers the expected exception, letting us know that
+ * the code that *would* have set it to disabled realizes it has already been set to sandboxed.
+ *
+ * This at least lets us know that the configuration attribute gets picked up and used.
+ */
+ @Test(expected = RuntimeException.class)
+ public void shouldBeAbleToDisableSandboxing() throws Exception
+ {
+ // NOTE: This has to be initialized to sandboxed, because it can only be initialized once per JVM session,
+ // and all other tests depend on it being sandboxed.
+ GlobalJavascriptInitializer.initialize( GlobalJavascriptInitializer.Mode.SANDBOXED );
+
+ server = server().withProperty( ServerSettings.script_sandboxing_enabled.name(), "false" )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+
+ // When
+ server.start();
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/TransactionTimeoutIT.java b/community/server/src/test/java/org/neo4j/server/TransactionTimeoutIT.java
new file mode 100644
index 0000000000000..ca4e255edd9d4
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/TransactionTimeoutIT.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.After;
+import org.junit.Test;
+
+import org.neo4j.server.configuration.ServerSettings;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+import org.neo4j.test.server.HTTP;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.neo4j.helpers.collection.MapUtil.map;
+import static org.neo4j.kernel.api.exceptions.Status.Transaction.TransactionNotFound;
+import static org.neo4j.server.helpers.CommunityServerBuilder.server;
+
+public class TransactionTimeoutIT extends ExclusiveServerTestBase
+{
+ private CommunityNeoServer server;
+
+ @After
+ public void stopTheServer()
+ {
+ server.stop();
+ }
+
+ @Test
+ public void shouldHonorReallyLowSessionTimeout() throws Exception
+ {
+ // Given
+ server = server().withProperty( ServerSettings.transaction_timeout.name(), "1" ).build();
+ server.start();
+
+ String tx = HTTP.POST( txURI(), asList( map( "statement", "CREATE (n)" ) ) ).location();
+
+ // When
+ Thread.sleep( 1000 * 5 );
+ Map response = HTTP.POST( tx + "/commit" ).content();
+
+ // Then
+ @SuppressWarnings("unchecked")
+ List> errors = (List>) response.get( "errors" );
+ assertThat( (String) errors.get( 0 ).get( "code" ), equalTo( TransactionNotFound.code().serialize() ) );
+ }
+
+ private String txURI()
+ {
+ return server.baseUri().toString() + "db/data/transaction";
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/AutoIndexIT.java b/community/server/src/test/java/org/neo4j/server/rest/AutoIndexIT.java
new file mode 100644
index 0000000000000..d92ec11b05a1c
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/AutoIndexIT.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.web.RestfulGraphDatabase;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+import org.neo4j.test.GraphDescription.REL;
+
+import static org.hamcrest.Matchers.hasItem;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class AutoIndexIT extends AbstractRestFunctionalTestBase
+{
+ /**
+ * Find node by query from an automatic index.
+ *
+ * See Find node by query for the actual query syntax.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void shouldRetrieveFromAutoIndexByQuery()
+ {
+ data.get();
+ assertSize( 1, gen.get()
+ .expectedStatus( 200 )
+ .get( nodeAutoIndexUri() + "?query=name:I" )
+ .entity() );
+ }
+
+ private String nodeAutoIndexUri()
+ {
+ return getDataUri() + "index/auto/node/";
+ }
+
+ /**
+ * Automatic index nodes can be found via exact lookups with normal Index
+ * REST syntax.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void find_node_by_exact_match_from_an_automatic_index()
+ {
+ data.get();
+ assertSize( 1, gen.get()
+ .expectedStatus( 200 )
+ .get( nodeAutoIndexUri() + "name/I" )
+ .entity() );
+ }
+
+ /**
+ * The automatic relationship index can not be removed.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void Relationship_AutoIndex_is_not_removable()
+ {
+ data.get();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( relationshipAutoIndexUri() )
+ .entity();
+ }
+
+ /**
+ * The automatic node index can not be removed.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void AutoIndex_is_not_removable()
+ {
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( nodeAutoIndexUri() )
+ .entity();
+ }
+
+ /**
+ * It is not allowed to add items manually to automatic indexes.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void items_can_not_be_added_manually_to_an_AutoIndex() throws Exception
+ {
+ data.get();
+ String indexName;
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ indexName = graphdb().index().getNodeAutoIndexer().getAutoIndex().getName();
+ tx.success();
+ }
+
+ gen.get()
+ .expectedStatus( 405 )
+ .payload( createJsonStringFor( getNodeUri( data.get()
+ .get( "I" ) ), "name", "I" ) )
+ .post( postNodeIndexUri( indexName ) )
+ .entity();
+
+ }
+
+ private String createJsonStringFor( final String targetUri, final String key, final String value )
+ {
+ return "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\": \"" + targetUri + "\"}";
+ }
+
+ /**
+ * It is not allowed to add items manually to automatic indexes.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I" ), @NODE( name = "you" )}, relationships = {@REL( start = "I", end = "you",
+ type = "know", properties = {@PROP( key = "since", value = "today" )} )}, autoIndexRelationships = true )
+ public void items_can_not_be_added_manually_to_a_Relationship_AutoIndex() throws Exception
+ {
+ data.get();
+ String indexName;
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ indexName = graphdb().index().getRelationshipAutoIndexer().getAutoIndex().getName();
+ tx.success();
+ }
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ gen.get()
+ .expectedStatus( 405 )
+ .payload( createJsonStringFor( getRelationshipUri( data.get()
+ .get( "I" )
+ .getRelationships()
+ .iterator()
+ .next() ), "name", "I" ) )
+ .post( postRelationshipIndexUri( indexName ) )
+ .entity();
+ }
+ }
+
+ /**
+ * It is not allowed to remove entries manually from automatic indexes.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I", setNameProperty = true )}, autoIndexNodes = true )
+ public void autoindexed_items_cannot_be_removed_manually()
+ {
+ long id = data.get()
+ .get( "I" )
+ .getId();
+ String indexName;
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ indexName = graphdb().index().getNodeAutoIndexer().getAutoIndex().getName();
+ tx.success();
+ }
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/node/" + indexName + "/name/I/" + id )
+ .entity();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/node/" + indexName + "/name/" + id )
+ .entity();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/node/" + indexName + "/" + id )
+ .entity();
+ }
+
+ /**
+ * It is not allowed to remove entries manually from automatic indexes.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I" ), @NODE( name = "you" )}, relationships = {@REL( start = "I", end = "you",
+ type = "know", properties = {@PROP( key = "since", value = "today" )} )}, autoIndexRelationships = true )
+ public void autoindexed_relationships_cannot_be_removed_manually()
+ {
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ data.get();
+ tx.success();
+ }
+
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ long id = data.get()
+ .get( "I" )
+ .getRelationships()
+ .iterator()
+ .next()
+ .getId();
+ String indexName = graphdb().index()
+ .getRelationshipAutoIndexer()
+ .getAutoIndex()
+ .getName();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/relationship/" + indexName + "/since/today/" + id )
+ .entity();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/relationship/" + indexName + "/since/" + id )
+ .entity();
+ gen.get()
+ .expectedStatus( 405 )
+ .delete( getDataUri() + "index/relationship/" + indexName + "/" + id )
+ .entity();
+ }
+ }
+
+ /**
+ * See the example request.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I" ), @NODE( name = "you" )}, relationships = {@REL( start = "I", end = "you",
+ type = "know", properties = {@PROP( key = "since", value = "today" )} )}, autoIndexRelationships = true )
+ public void Find_relationship_by_query_from_an_automatic_index()
+ {
+ data.get();
+ assertSize( 1, gen.get()
+ .expectedStatus( 200 )
+ .get( relationshipAutoIndexUri() + "?query=since:today" )
+ .entity() );
+ }
+
+ /**
+ * See the example request.
+ */
+ @Test
+ @Graph( nodes = {@NODE( name = "I" ), @NODE( name = "you" )}, relationships = {@REL( start = "I", end = "you",
+ type = "know", properties = {@PROP( key = "since", value = "today" )} )}, autoIndexRelationships = true )
+ public void Find_relationship_by_exact_match_from_an_automatic_index()
+ {
+ data.get();
+ assertSize( 1, gen.get()
+ .expectedStatus( 200 )
+ .get( relationshipAutoIndexUri() + "since/today/" )
+ .entity() );
+ }
+
+ /**
+ * Get current status for autoindexing on nodes.
+ */
+ @Test
+ public void getCurrentStatusForNodes()
+ {
+ setEnabledAutoIndexingForType( "node", false );
+ checkAndAssertAutoIndexerIsEnabled( "node", false );
+ }
+
+ /**
+ * Enable node autoindexing.
+ */
+ @Test
+ public void enableNodeAutoIndexing()
+ {
+ setEnabledAutoIndexingForType( "node", true );
+ checkAndAssertAutoIndexerIsEnabled( "node", true );
+ }
+
+ /**
+ * Add a property for autoindexing on nodes.
+ */
+ @Test
+ public void addAutoIndexingPropertyForNodes()
+ {
+ gen.get()
+ .expectedStatus( 204 )
+ .payload( "myProperty1" )
+ .post( autoIndexURI( "node" ) + "/properties" );
+ }
+
+ /**
+ * Lookup list of properties being autoindexed.
+ */
+ @Test
+ public void listAutoIndexingPropertiesForNodes() throws JsonParseException
+ {
+ int initialPropertiesSize = getAutoIndexedPropertiesForType( "node" ).size();
+
+ String propName = "some-property" + System.currentTimeMillis();
+ server().getDatabase().getGraph().index().getNodeAutoIndexer().startAutoIndexingProperty( propName );
+
+ List properties = getAutoIndexedPropertiesForType( "node" );
+
+ assertEquals( initialPropertiesSize + 1, properties.size() );
+ assertThat( properties, hasItem( propName ) );
+ }
+
+ /**
+ * Remove a property for autoindexing on nodes.
+ */
+ @Test
+ public void removeAutoIndexingPropertyForNodes()
+ {
+ gen.get()
+ .expectedStatus( 204 )
+ .delete( autoIndexURI( "node" ) + "/properties/myProperty1" );
+ }
+
+ @Test
+ public void switchOnOffAutoIndexingForNodes()
+ {
+ switchOnOffAutoIndexingForType( "node" );
+ }
+
+ @Test
+ public void switchOnOffAutoIndexingForRelationships()
+ {
+ switchOnOffAutoIndexingForType( "relationship" );
+ }
+
+ @Test
+ public void addRemoveAutoIndexedPropertyForNodes() throws JsonParseException
+ {
+ addRemoveAutoIndexedPropertyForType( "node" );
+ }
+
+ @Test
+ public void addRemoveAutoIndexedPropertyForRelationships() throws JsonParseException
+ {
+ addRemoveAutoIndexedPropertyForType( "relationship" );
+ }
+
+ private String relationshipAutoIndexUri()
+ {
+ return getDataUri() + "index/auto/relationship/";
+ }
+
+ private void addRemoveAutoIndexedPropertyForType( String uriPartForType ) throws JsonParseException
+ {
+ int intialPropertiesSize = getAutoIndexedPropertiesForType( uriPartForType ).size();
+
+ long millis = System.currentTimeMillis();
+ String myProperty1 = uriPartForType + "-myProperty1-" + millis;
+ String myProperty2 = uriPartForType + "-myProperty2-" + millis;
+
+ gen.get()
+ .expectedStatus( 204 )
+ .payload( myProperty1 )
+ .post( autoIndexURI( uriPartForType ) + "/properties" );
+ gen.get()
+ .expectedStatus( 204 )
+ .payload( myProperty2 )
+ .post( autoIndexURI( uriPartForType ) + "/properties" );
+
+ List properties = getAutoIndexedPropertiesForType( uriPartForType );
+ assertEquals( intialPropertiesSize + 2, properties.size() );
+ assertTrue( properties.contains( myProperty1 ) );
+ assertTrue( properties.contains( myProperty2 ) );
+
+ gen.get()
+ .expectedStatus( 204 )
+ .payload( null )
+ .delete( autoIndexURI( uriPartForType )
+ + "/properties/" + myProperty2 );
+
+ properties = getAutoIndexedPropertiesForType( uriPartForType );
+ assertEquals( intialPropertiesSize + 1, properties.size() );
+ assertTrue( properties.contains( myProperty1 ) );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ private List getAutoIndexedPropertiesForType( String uriPartForType )
+ throws JsonParseException
+ {
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .get( autoIndexURI( uriPartForType ) + "/properties" ).entity();
+ return (List) JsonHelper.readJson( result );
+ }
+
+ private void switchOnOffAutoIndexingForType( String uriPartForType )
+ {
+ setEnabledAutoIndexingForType( uriPartForType, true );
+ checkAndAssertAutoIndexerIsEnabled( uriPartForType, true );
+ setEnabledAutoIndexingForType( uriPartForType, false );
+ checkAndAssertAutoIndexerIsEnabled( uriPartForType, false );
+ }
+
+ private void setEnabledAutoIndexingForType( String uriPartForType, boolean enabled )
+ {
+ gen.get()
+ .expectedStatus( 204 )
+ .payload( Boolean.toString( enabled ) )
+ .put( autoIndexURI( uriPartForType ) + "/status" );
+ }
+
+ private void checkAndAssertAutoIndexerIsEnabled( String uriPartForType, boolean enabled )
+ {
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .get( autoIndexURI( uriPartForType ) + "/status" ).entity();
+ assertEquals( enabled, Boolean.parseBoolean( result ) );
+ }
+
+ private String autoIndexURI( String type )
+ {
+ return getDataUri()
+ + RestfulGraphDatabase.PATH_AUTO_INDEX.replace( "{type}", type );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/AutoIndexWithNonDefaultConfigurationThroughRESTAPIIT.java b/community/server/src/test/java/org/neo4j/server/rest/AutoIndexWithNonDefaultConfigurationThroughRESTAPIIT.java
new file mode 100644
index 0000000000000..b73381c516417
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/AutoIndexWithNonDefaultConfigurationThroughRESTAPIIT.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.helpers.ServerHelper;
+import org.neo4j.test.TestData;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+
+public class AutoIndexWithNonDefaultConfigurationThroughRESTAPIIT extends ExclusiveServerTestBase
+{
+ private static CommunityNeoServer server;
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @ClassRule
+ public static TemporaryFolder staticFolder = new TemporaryFolder();
+
+ public
+ @Rule
+ TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+
+ @Before
+ public void setUp()
+ {
+ gen.get().setSection( "dev/rest-api" );
+ }
+
+ @BeforeClass
+ public static void allocateServer() throws IOException
+ {
+ server = CommunityServerBuilder.server()
+ .usingDataDir( staticFolder.getRoot().getAbsolutePath() )
+ .withAutoIndexingEnabledForNodes( "foo", "bar" )
+ .build();
+ server.start();
+ functionalTestHelper = new FunctionalTestHelper( server );
+ }
+
+ @Before
+ public void cleanTheDatabase()
+ {
+ ServerHelper.cleanTheDatabase( server );
+ }
+
+ @AfterClass
+ public static void stopServer()
+ {
+ server.stop();
+ }
+
+ /**
+ * Create an auto index for nodes with specific configuration.
+ */
+ @Test
+ public void shouldCreateANodeAutoIndexWithGivenFullTextConfiguration() throws Exception
+ {
+ String responseBody = gen.get()
+ .expectedStatus( 201 )
+ .payload( "{\"name\":\"node_auto_index\", \"config\":{\"type\":\"fulltext\",\"provider\":\"lucene\"}}" )
+ .post( functionalTestHelper.nodeIndexUri() )
+ .entity();
+
+ assertThat( responseBody, containsString( "\"type\" : \"fulltext\"" ) );
+ }
+
+ /**
+ * Create an auto index for relationships with specific configuration.
+ */
+ @Test
+ public void shouldCreateARelationshipAutoIndexWithGivenFullTextConfiguration() throws Exception
+ {
+ String responseBody = gen.get()
+ .expectedStatus( 201 )
+ .payload(
+ "{\"name\":\"relationship_auto_index\", \"config\":{\"type\":\"fulltext\"," +
+ "\"provider\":\"lucene\"}}" )
+ .post( functionalTestHelper.relationshipIndexUri() )
+ .entity();
+
+ assertThat( responseBody, containsString( "\"type\" : \"fulltext\"" ) );
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/BatchOperationIT.java b/community/server/src/test/java/org/neo4j/server/rest/BatchOperationIT.java
new file mode 100644
index 0000000000000..3c1af928f9221
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/BatchOperationIT.java
@@ -0,0 +1,773 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import org.codehaus.jackson.JsonNode;
+import org.json.JSONException;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.ServerTestUtils;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription.Graph;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.neo4j.graphdb.Neo4jMatchers.hasProperty;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+
+public class BatchOperationIT extends AbstractRestFunctionalDocTestBase
+{
+
+ @Documented( "Execute multiple operations in batch.\n" +
+ "\n" +
+ "The batch service expects an array of job descriptions as input, each job\n" +
+ "description describing an action to be performed via the normal server\n" +
+ "API.\n" +
+ "\n" +
+ "Each job description should contain a +to+ attribute, with a value\n" +
+ "relative to the data API root (so http://localhost:7474/db/data/node becomes\n" +
+ "just /node), and a +method+ attribute containing HTTP verb to use.\n" +
+ "\n" +
+ "Optionally you may provide a +body+ attribute, and an +id+ attribute to\n" +
+ "help you keep track of responses, although responses are guaranteed to be\n" +
+ "returned in the same order the job descriptions are received.\n" +
+ "\n" +
+ "The following figure outlines the different parts of the job\n" +
+ "descriptions:\n" +
+ "\n" +
+ "image::batch-request-api.png[]" )
+ @SuppressWarnings( "unchecked" )
+ @Test
+ @Graph("Joe knows John")
+ public void shouldPerformMultipleOperations() throws Exception
+ {
+ long idJoe = data.get().get( "Joe" ).getId();
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("PUT")
+ .key("to") .value("/node/" + idJoe + "/properties")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(0)
+ .endObject()
+ .object()
+ .key("method") .value("GET")
+ .key("to") .value("/node/" + idJoe)
+ .key("id") .value(1)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(2)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(3)
+ .endObject()
+ .endArray().toString();
+
+
+ String entity = gen.get()
+ .description( startGraph( "execute multiple operations in batch" ) )
+ .payload(jsonString)
+ .expectedStatus(200)
+ .post(batchUri()).entity();
+
+ List> results = JsonHelper.jsonToList(entity);
+
+ assertEquals(4, results.size());
+
+ Map putResult = results.get(0);
+ Map getResult = results.get(1);
+ Map firstPostResult = results.get(2);
+ Map secondPostResult = results.get(3);
+
+ // Ids should be ok
+ assertEquals(0, putResult.get("id"));
+ assertEquals(2, firstPostResult.get("id"));
+ assertEquals(3, secondPostResult.get("id"));
+
+ // Should contain "from"
+ assertEquals("/node/"+idJoe+"/properties", putResult.get("from"));
+ assertEquals("/node/"+idJoe, getResult.get("from"));
+ assertEquals("/node", firstPostResult.get("from"));
+ assertEquals("/node", secondPostResult.get("from"));
+
+ // Post should contain location
+ assertTrue(((String) firstPostResult.get("location")).length() > 0);
+ assertTrue(((String) secondPostResult.get("location")).length() > 0);
+
+ // Should have created by the first PUT request
+ Map body = (Map) getResult.get("body");
+ assertEquals(1, ((Map) body.get("data")).get("age"));
+
+
+ }
+
+ @Documented( "Refer to items created earlier in the same batch job.\n" +
+ "\n" +
+ "The batch operation API allows you to refer to the URI returned from a\n" +
+ "created resource in subsequent job descriptions, within the same batch\n" +
+ "call.\n" +
+ "\n" +
+ "Use the +{[JOB ID]}+ special syntax to inject URIs from created resources\n" +
+ "into JSON strings in subsequent job descriptions." )
+ @Test
+ public void shouldBeAbleToReferToCreatedResource() throws Exception
+ {
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("id") .value(0)
+ .key("body")
+ .object()
+ .key("name").value("bob")
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("id") .value(1)
+ .key("body")
+ .object()
+ .key("age").value(12)
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("{0}/relationships")
+ .key("id") .value(3)
+ .key("body")
+ .object()
+ .key("to").value("{1}")
+ .key("data")
+ .object()
+ .key("since").value("2010")
+ .endObject()
+ .key("type").value("KNOWS")
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/relationship/my_rels")
+ .key("id") .value(4)
+ .key("body")
+ .object()
+ .key("key").value("since")
+ .key("value").value("2010")
+ .key("uri").value("{3}")
+ .endObject()
+ .endObject()
+ .endArray().toString();
+
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .payload( jsonString )
+ .post( batchUri() )
+ .entity();
+
+ List> results = JsonHelper.jsonToList(entity);
+
+ assertEquals(4, results.size());
+
+// String rels = gen.get()
+// .expectedStatus( 200 ).get( getRelationshipIndexUri( "my_rels", "since", "2010")).entity();
+// assertEquals(1, JsonHelper.jsonToList( rels ).size());
+ }
+
+ private String batchUri()
+ {
+ return getDataUri()+"batch";
+ }
+
+ @Test
+ public void shouldGetLocationHeadersWhenCreatingThings() throws Exception
+ {
+ int originalNodeCount = countNodes();
+
+ final String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method").value("POST")
+ .key("to").value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .endObject()
+ .endArray().toString();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(200, response.getStatus());
+ assertEquals(originalNodeCount + 1, countNodes());
+
+ List> results = JsonHelper.jsonToList(response.getEntity());
+
+ assertEquals(1, results.size());
+
+ Map result = results.get(0);
+ assertTrue(((String) result.get("location")).length() > 0);
+ }
+
+ @Test
+ public void shouldForwardUnderlyingErrors() throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().post(batchUri(), new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age")
+ .array()
+ .value(true)
+ .value("hello")
+ .endArray()
+ .endObject()
+ .endObject()
+ .endArray()
+ .toString());
+ assertEquals(500, response.getStatus());
+ Map res = JsonHelper.jsonToMap(response.getEntity());
+
+ assertTrue(((String)res.get("message")).startsWith("Invalid JSON array in POST body"));
+ }
+
+ @Test
+ public void shouldRollbackAllWhenGivenIncorrectRequest() throws ClientHandlerException,
+ UniformInterfaceException, JSONException
+ {
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value("1")
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .array()
+ .value("a_list")
+ .value("this_makes_no_sense")
+ .endArray()
+ .endObject()
+ .endArray()
+ .toString();
+
+ int originalNodeCount = countNodes();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(500, response.getStatus());
+ assertEquals(originalNodeCount, countNodes());
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void shouldHandleUnicodeGetCorrectly() throws Exception
+ {
+ String asianText = "\u4f8b\u5b50";
+ String germanText = "öäüÖÄÜß";
+
+ String complicatedString = asianText + germanText;
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body") .object()
+ .key(complicatedString).value(complicatedString)
+ .endObject()
+ .endObject()
+ .endArray()
+ .toString();
+
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .payload( jsonString )
+ .post( batchUri() )
+ .entity();
+
+ // Pull out the property value from the depths of the response
+ Map response = (Map) JsonHelper.jsonToList(entity).get(0).get("body");
+ String returnedValue = (String)((Map)response.get("data")).get(complicatedString);
+
+ // Ensure nothing was borked.
+ assertThat("Expected twisted unicode case to work, but response was: " + entity,
+ returnedValue, is(complicatedString));
+ }
+
+ @Test
+ public void shouldHandleFailingCypherStatementCorrectly() throws Exception
+ {
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/cypher")
+ .key("body") .object()
+ .key("query").value("create (n) set n.foo = {maps:'not welcome'} return n")
+ .key("params").object().key("id").value("0").endObject()
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .endObject()
+ .endArray()
+ .toString();
+
+ String entity = gen.get()
+ .expectedStatus( 500 )
+ .payload( jsonString )
+ .post( batchUri() )
+ .entity();
+
+ // Pull out the property value from the depths of the response
+ Map result = JsonHelper.jsonToMap(entity);
+ String exception = (String) result.get("exception");
+ assertThat(exception, is("BatchOperationFailedException"));
+ String innerException = (String) ((Map) JsonHelper.jsonToMap((String) result.get("message"))).get("exception");
+ assertThat(innerException, is("CypherTypeException"));
+ }
+
+ @Test
+ @Graph("Peter likes Jazz")
+ public void shouldHandleEscapedStrings() throws ClientHandlerException,
+ UniformInterfaceException, JSONException, JsonParseException
+ {
+ String string = "Jazz";
+ Node gnode = getNode( string );
+ assertThat( gnode, inTx(graphdb(), hasProperty( "name" ).withValue(string)) );
+
+ String name = "string\\ and \"test\"";
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("PUT")
+ .key("to") .value("/node/"+gnode.getId()+"/properties")
+ .key("body")
+ .object()
+ .key("name").value(name)
+ .endObject()
+ .endObject()
+ .endArray()
+ .toString();
+ gen.get()
+ .expectedStatus( 200 )
+ .payload( jsonString )
+ .post( batchUri() )
+ .entity();
+
+ jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("GET")
+ .key("to") .value("/node/"+gnode.getId()+"/properties/name")
+ .endObject()
+ .endArray()
+ .toString();
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .payload( jsonString )
+ .post( batchUri() )
+ .entity();
+
+ List> results = JsonHelper.jsonToList(entity);
+ assertEquals(results.get(0).get("body"), name);
+ }
+
+ @Test
+ public void shouldRollbackAllWhenInsertingIllegalData() throws ClientHandlerException,
+ UniformInterfaceException, JSONException
+ {
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .endObject()
+
+ .object()
+ .key("method").value("POST")
+ .key("to").value("/node")
+ .key("body")
+ .object()
+ .key("age")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .endObject()
+ .endObject()
+
+ .endArray().toString();
+
+ int originalNodeCount = countNodes();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(500, response.getStatus());
+ assertEquals(originalNodeCount, countNodes());
+
+ }
+
+ @Test
+ public void shouldRollbackAllOnSingle404() throws ClientHandlerException,
+ UniformInterfaceException, JSONException
+ {
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("www.google.com")
+ .endObject()
+
+ .endArray().toString();
+
+ int originalNodeCount = countNodes();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(500, response.getStatus());
+ assertEquals(originalNodeCount, countNodes());
+ }
+
+ @Test
+ public void shouldBeAbleToReferToUniquelyCreatedEntities() throws Exception
+ {
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/node/Cultures?unique")
+ .key("body")
+ .object()
+ .key("key").value("ID")
+ .key("value").value("fra")
+ .key("properties")
+ .object()
+ .key("ID").value("fra")
+ .endObject()
+ .endObject()
+ .key("id") .value(0)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("id") .value(1)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("{1}/relationships")
+ .key("body")
+ .object()
+ .key("to").value("{0}")
+ .key("type").value("has")
+ .endObject()
+ .key("id") .value(2)
+ .endObject()
+ .endArray().toString();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(200, response.getStatus());
+
+ }
+
+ @Test
+ public void shouldNotFailWhenRemovingAndAddingLabelsInOneBatch() throws Exception
+ {
+ // given
+
+ /*
+ curl -X POST http://localhost:7474/db/data/batch -H 'Content-Type: application/json'
+ -d '[
+ {"body":{"name":"Alice"},"to":"node","id":0,"method":"POST"},
+ {"body":["expert","coder"],"to":"{0}/labels","id":1,"method":"POST"},
+ {"body":["novice","chef"],"to":"{0}/labels","id":2,"method":"PUT"}
+ ]'
+ */
+
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("node")
+ .key("id") .value(0)
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("Alice")
+ .endObject()
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("{0}/labels")
+ .key("id") .value(1)
+ .key("body")
+ .array()
+ .value( "expert" )
+ .value( "coder" )
+ .endArray()
+ .endObject()
+ .object()
+ .key("method") .value("PUT")
+ .key("to") .value("{0}/labels")
+ .key("id") .value(2)
+ .key("body")
+ .array()
+ .value( "novice" )
+ .value( "chef" )
+ .endArray()
+ .endObject()
+ .endArray().toString();
+
+ // when
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ // then
+ assertEquals(200, response.getStatus());
+ }
+
+ // It has to be possible to create relationships among created and not-created nodes
+ // in batch operation. Tests the fix for issue #690.
+ @Test
+ public void shouldBeAbleToReferToNotCreatedUniqueEntities() throws Exception
+ {
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/node/Cultures?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("tobias")
+ .key("properties")
+ .object()
+ .key("name").value("Tobias Tester")
+ .endObject()
+ .endObject()
+ .key("id") .value(0)
+ .endObject()
+ .object() // Creates Andres, hence 201 Create
+ .key("method") .value("POST")
+ .key("to") .value("/index/node/Cultures?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("andres")
+ .key("properties")
+ .object()
+ .key("name").value("Andres Tester")
+ .endObject()
+ .endObject()
+ .key("id") .value(1)
+ .endObject()
+ .object() // Duplicated to ID.1, hence 200 OK
+ .key("method") .value("POST")
+ .key("to") .value("/index/node/Cultures?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("andres")
+ .key("properties")
+ .object()
+ .key("name").value("Andres Tester")
+ .endObject()
+ .endObject()
+ .key("id") .value(2)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/relationship/my_rels/?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("tobias-andres")
+ .key("start").value("{0}")
+ .key("end").value("{1}")
+ .key("type").value("FRIENDS")
+ .endObject()
+ .key("id") .value(3)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/relationship/my_rels/?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("andres-tobias")
+ .key("start").value("{2}") // Not-created entity here
+ .key("end").value("{0}")
+ .key("type").value("FRIENDS")
+ .endObject()
+ .key("id") .value(4)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/index/relationship/my_rels/?unique")
+ .key("body")
+ .object()
+ .key("key").value("name")
+ .key("value").value("andres-tobias")
+ .key("start").value("{1}") // Relationship should not be created
+ .key("end").value("{0}")
+ .key("type").value("FRIENDS")
+ .endObject()
+ .key("id") .value(5)
+ .endObject()
+ .endArray().toString();
+
+ JaxRsResponse response = RestRequest.req().post(batchUri(), jsonString);
+
+ assertEquals(200, response.getStatus());
+
+ final String entity = response.getEntity();
+ List> results = JsonHelper.jsonToList(entity);
+ assertEquals(6, results.size());
+ Map andresResult1 = results.get(1);
+ Map andresResult2 = results.get(2);
+ Map secondRelationship = results.get(4);
+ Map thirdRelationship = results.get(5);
+
+ // Same people
+ Map body1 = (Map) andresResult1.get("body");
+ Map body2 = (Map) andresResult2.get("body");
+ assertEquals(body1.get("id"), body2.get("id"));
+ // Same relationship
+ body1 = (Map) secondRelationship.get("body");
+ body2 = (Map) thirdRelationship.get("body");
+ assertEquals(body1.get("self"), body2.get("self"));
+ // Created for {2} {0}
+ assertTrue(((String) secondRelationship.get("location")).length() > 0);
+ // {2} = {1} = Andres
+ body1 = (Map) secondRelationship.get("body");
+ body2 = (Map) andresResult1.get("body");
+ assertEquals(body1.get("start"), body2.get("self"));
+ }
+
+ @Test
+ public void shouldFailWhenUsingPeriodicCommitViaNewTxEndpoint() throws Exception
+ {
+ ServerTestUtils.withCSVFile( 1, new ServerTestUtils.BlockWithCSVFileURL()
+ {
+ @Override
+ public void execute( String url ) throws Exception
+ {
+ // Given
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key( "method" ).value("POST")
+ .key( "to" ).value("/transaction/commit")
+ .key( "body" ).object()
+ .key("statements").array()
+ .object().key( "statement" ).value( "USING PERIODIC COMMIT LOAD CSV FROM '" + url + "' AS line CREATE ()" ).endObject()
+ .endArray()
+ .endObject()
+ .endObject()
+ .endArray()
+ .toString();
+
+ // When
+ JsonNode result = JsonHelper.jsonNode(gen.get()
+ .expectedStatus(200)
+ .payload(jsonString)
+ .post(batchUri())
+ .entity());
+
+ // Then
+ JsonNode results = result.get(0).get("body").get("results");
+ JsonNode errors = result.get(0).get("body").get("errors");
+
+ assertTrue( "Results not an array", results.isArray() );
+ assertEquals( 0, results.size() );
+ assertTrue( "Errors not an array", errors.isArray() );
+ assertEquals( 1, errors.size() );
+
+ String errorCode = errors.get(0).get("code").getTextValue();
+ assertEquals( "Neo.ClientError.Statement.SemanticError", errorCode );
+ }
+ } );
+ }
+
+ private int countNodes()
+ {
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ int count = 0;
+ for(Node node : graphdb().getAllNodes())
+ {
+ count++;
+ }
+ return count;
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/CompactJsonIT.java b/community/server/src/test/java/org/neo4j/server/rest/CompactJsonIT.java
new file mode 100644
index 0000000000000..b596ecfdf6c9a
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/CompactJsonIT.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.util.Collections;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.repr.formats.CompactJsonFormat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class CompactJsonIT extends AbstractRestFunctionalTestBase
+{
+ private long thomasAnderson;
+ private long trinity;
+ private long thomasAndersonLovesTrinity;
+
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Before
+ public void setupTheDatabase()
+ {
+ createTheMatrix();
+ }
+
+ private void createTheMatrix()
+ {
+ // Create the matrix example
+ thomasAnderson = createAndIndexNode( "Thomas Anderson" );
+ trinity = createAndIndexNode( "Trinity" );
+ long tank = createAndIndexNode( "Tank" );
+
+ long knowsRelationshipId = helper.createRelationship( "KNOWS", thomasAnderson, trinity );
+ thomasAndersonLovesTrinity = helper.createRelationship( "LOVES", thomasAnderson, trinity );
+ helper.setRelationshipProperties( thomasAndersonLovesTrinity,
+ Collections.singletonMap( "strength", (Object) 100 ) );
+ helper.createRelationship( "KNOWS", thomasAnderson, tank );
+ helper.createRelationship( "KNOWS", trinity, tank );
+
+ // index a relationship
+ helper.createRelationshipIndex( "relationships" );
+ helper.addRelationshipToIndex( "relationships", "key", "value", knowsRelationshipId );
+
+ // index a relationship
+ helper.createRelationshipIndex( "relationships2" );
+ helper.addRelationshipToIndex( "relationships2", "key2", "value2", knowsRelationshipId );
+ }
+
+ private long createAndIndexNode( String name )
+ {
+ long id = helper.createNode();
+ helper.setNodeProperties( id, Collections.singletonMap( "name", (Object) name ) );
+ helper.addNodeToIndex( "node", "name", name, id );
+ return id;
+ }
+
+ @Test
+ public void shouldGetThomasAndersonDirectly() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.nodeUri(thomasAnderson), CompactJsonFormat.MEDIA_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ String entity = response.getEntity();
+ assertTrue(entity.contains("Thomas Anderson"));
+ assertValidJson(entity);
+ response.close();
+ }
+
+ private void assertValidJson( String entity )
+ {
+ try
+ {
+ assertTrue( JsonHelper.jsonToMap( entity )
+ .containsKey( "self" ) );
+ assertFalse( JsonHelper.jsonToMap( entity )
+ .containsKey( "properties" ) );
+ }
+ catch ( JsonParseException e )
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/ConfigureBaseUriIT.java b/community/server/src/test/java/org/neo4j/server/rest/ConfigureBaseUriIT.java
new file mode 100644
index 0000000000000..0f3bcd9f745d8
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/ConfigureBaseUriIT.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ConfigureBaseUriIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Test
+ public void shouldForwardHttpAndHost() throws Exception
+ {
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "X-Forwarded-Host", "foobar.com" );
+ httpget.setHeader( "X-Forwarded-Proto", "http" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertTrue( responseEntityBody.contains( "http://foobar.com" ) );
+ assertFalse( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+
+ }
+
+ @Test
+ public void shouldForwardHttpsAndHost() throws Exception
+ {
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "X-Forwarded-Host", "foobar.com" );
+ httpget.setHeader( "X-Forwarded-Proto", "https" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertTrue( responseEntityBody.contains( "https://foobar.com" ) );
+ assertFalse( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Test
+ public void shouldForwardHttpAndHostOnDifferentPort() throws Exception
+ {
+
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "X-Forwarded-Host", "foobar.com:9999" );
+ httpget.setHeader( "X-Forwarded-Proto", "http" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertTrue( responseEntityBody.contains( "http://foobar.com:9999" ) );
+ assertFalse( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Test
+ public void shouldForwardHttpAndFirstHost() throws Exception
+ {
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "X-Forwarded-Host", "foobar.com, bazbar.com" );
+ httpget.setHeader( "X-Forwarded-Proto", "http" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertTrue( responseEntityBody.contains( "http://foobar.com" ) );
+ assertFalse( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+
+ }
+
+ @Test
+ public void shouldForwardHttpsAndHostOnDifferentPort() throws Exception
+ {
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "X-Forwarded-Host", "foobar.com:9999" );
+ httpget.setHeader( "X-Forwarded-Proto", "https" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertTrue( responseEntityBody.contains( "https://foobar.com:9999" ) );
+ assertFalse( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+
+ @Test
+ public void shouldUseRequestUriWhenNoXForwardHeadersPresent() throws Exception
+ {
+ URI rootUri = functionalTestHelper.baseUri();
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( rootUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+
+ HttpResponse response = httpclient.execute( httpget );
+
+ String length = response.getHeaders( "CONTENT-LENGTH" )[0].getValue();
+ byte[] data = new byte[Integer.valueOf( length )];
+ response.getEntity().getContent().read( data );
+
+ String responseEntityBody = new String( data );
+
+ assertFalse( responseEntityBody.contains( "https://foobar.com" ) );
+ assertFalse( responseEntityBody.contains( ":0" ) );
+ assertTrue( responseEntityBody.contains( "localhost" ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/CreateRelationshipTest.java b/community/server/src/test/java/org/neo4j/server/rest/CreateRelationshipTest.java
new file mode 100644
index 0000000000000..431c88207beb9
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/CreateRelationshipTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+
+import com.sun.jersey.api.client.ClientResponse.Status;
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.RelationshipType;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.repr.RelationshipRepresentationTest;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.TestData.Title;
+
+import static org.junit.Assert.assertTrue;
+
+public class CreateRelationshipTest extends AbstractRestFunctionalDocTestBase
+{
+ @Test
+ @Graph( "Joe knows Sara" )
+ @Documented( "Upon successful creation of a relationship, the new relationship is returned." )
+ @Title( "Create a relationship with properties" )
+ public void create_a_relationship_with_properties() throws Exception
+ {
+ String jsonString = "{\"to\" : \""
+ + getDataUri()
+ + "node/"
+ + getNode( "Sara" ).getId()
+ + "\", \"type\" : \"LOVES\", \"data\" : {\"foo\" : \"bar\"}}";
+ Node i = getNode( "Joe" );
+ gen.get().description( startGraph( "Add relationship with properties before" ) );
+ gen.get().expectedStatus(
+ Status.CREATED.getStatusCode() ).payload( jsonString ).post(
+ getNodeUri( i ) + "/relationships" );
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ assertTrue( i.hasRelationship( RelationshipType.withName( "LOVES" ) ) );
+ }
+ }
+
+ @Test
+ @Documented( "Upon successful creation of a relationship, the new relationship is returned." )
+ @Title( "Create relationship" )
+ @Graph( "Joe knows Sara" )
+ public void create_relationship() throws Exception
+ {
+ String jsonString = "{\"to\" : \""
+ + getDataUri()
+ + "node/"
+ + getNode( "Sara" ).getId()
+ + "\", \"type\" : \"LOVES\"}";
+ Node i = getNode( "Joe" );
+ String entity = gen.get().expectedStatus(
+ Status.CREATED.getStatusCode() ).payload( jsonString )
+ .description( startGraph( "create relationship" ) )
+ .post( getNodeUri( i ) + "/relationships" ).entity();
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ assertTrue( i.hasRelationship( RelationshipType.withName( "LOVES" ) ) );
+ }
+ assertProperRelationshipRepresentation( JsonHelper.jsonToMap( entity ) );
+ }
+
+ @Test
+ @Graph( "Joe knows Sara" )
+ public void shouldRespondWith404WhenStartNodeDoesNotExist()
+ {
+ String jsonString = "{\"to\" : \""
+ + getDataUri()
+ + "node/"
+ + getNode( "Joe" )
+ + "\", \"type\" : \"LOVES\", \"data\" : {\"foo\" : \"bar\"}}";
+ gen.get().expectedStatus(
+ Status.NOT_FOUND.getStatusCode() ).expectedType( MediaType.TEXT_HTML_TYPE ).payload( jsonString ).post(
+ getDataUri() + "/node/12345/relationships" ).entity();
+ }
+
+ @Test
+ @Graph( "Joe knows Sara" )
+ public void creating_a_relationship_to_a_nonexisting_end_node()
+ {
+ String jsonString = "{\"to\" : \""
+ + getDataUri()
+ + "node/"
+ + "999999\", \"type\" : \"LOVES\", \"data\" : {\"foo\" : \"bar\"}}";
+ gen.get().expectedStatus(
+ Status.BAD_REQUEST.getStatusCode() ).payload( jsonString ).post(
+ getNodeUri( getNode( "Joe" ) ) + "/relationships" ).entity();
+ }
+
+ @Test
+ @Graph( "Joe knows Sara" )
+ public void creating_a_loop_relationship()
+ throws Exception
+ {
+
+ Node joe = getNode( "Joe" );
+ String jsonString = "{\"to\" : \"" + getNodeUri( joe )
+ + "\", \"type\" : \"LOVES\"}";
+ String entity = gen.get().expectedStatus(
+ Status.CREATED.getStatusCode() ).payload( jsonString ).post(
+ getNodeUri( getNode( "Joe" ) ) + "/relationships" ).entity();
+ assertProperRelationshipRepresentation( JsonHelper.jsonToMap( entity ) );
+ }
+
+ @Test
+ @Graph( "Joe knows Sara" )
+ public void providing_bad_JSON()
+ {
+ String jsonString = "{\"to\" : \""
+ + getNodeUri( data.get().get( "Joe" ) )
+ + "\", \"type\" : \"LOVES\", \"data\" : {\"foo\" : **BAD JSON HERE*** \"bar\"}}";
+ gen.get().expectedStatus(
+ Status.BAD_REQUEST.getStatusCode() ).payload( jsonString ).post(
+ getNodeUri( getNode( "Joe" ) ) + "/relationships" ).entity();
+ }
+
+ private void assertProperRelationshipRepresentation(
+ Map relrep )
+ {
+ RelationshipRepresentationTest.verifySerialisation( relrep );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/CypherIT.java b/community/server/src/test/java/org/neo4j/server/rest/CypherIT.java
new file mode 100644
index 0000000000000..260ab780df23f
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/CypherIT.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+import java.util.Map;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.Pair;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.LABEL;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+import org.neo4j.test.GraphDescription.REL;
+import org.neo4j.test.TestData.Title;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.isA;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToMap;
+
+public class CypherIT extends AbstractRestFunctionalTestBase {
+
+ @Test
+ @Title( "Send a query" )
+ @Documented( "A simple query returning all nodes connected to some node, returning the node and the name " +
+ "property, if it exists, otherwise `NULL`:" )
+ @Graph( nodes = {
+ @NODE( name = "I", setNameProperty = true ),
+ @NODE( name = "you", setNameProperty = true ),
+ @NODE( name = "him", setNameProperty = true, properties = {
+ @PROP( key = "age", value = "25", type = GraphDescription.PropType.INTEGER ) } ) },
+ relationships = {
+ @REL( start = "I", end = "him", type = "know", properties = { } ),
+ @REL( start = "I", end = "you", type = "know", properties = { } ) } )
+ public void testPropertyColumn() throws UnsupportedEncodingException {
+ String script = createScript( "MATCH (x {name: 'I'})-[r]->(n) RETURN type(r), n.name, n.age" );
+
+ String response = cypherRestCall( script, Status.OK );
+
+ assertThat( response, containsString( "you" ) );
+ assertThat( response, containsString( "him" ) );
+ assertThat( response, containsString( "25" ) );
+ assertThat( response, not( containsString( "\"x\"" ) ) );
+ }
+
+ @Test
+ @Title( "Retrieve query metadata" )
+ @Documented("By passing in an additional GET parameter when you execute Cypher queries, metadata about the " +
+ "query will be returned, such as how many labels were added or removed by the query.")
+ @Graph( nodes = { @NODE( name = "I", setNameProperty = true, labels = { @LABEL( "Director" ) } ) } )
+ public void testQueryStatistics() throws JsonParseException
+ {
+ // Given
+ String script = createScript( "MATCH (n {name: 'I'}) SET n:Actor REMOVE n:Director RETURN labels(n)" );
+
+ // When
+ Map output = jsonToMap(doCypherRestCall( cypherUri() + "?includeStats=true", script, Status.OK ));
+
+ // Then
+ @SuppressWarnings("unchecked")
+ Map stats = (Map) output.get( "stats" );
+
+ assertThat( stats, isA( Map.class ) );
+ assertThat( (Boolean) stats.get( "contains_updates" ), is( true ) );
+ assertThat( (Integer) stats.get( "labels_added" ), is( 1 ) );
+ assertThat( (Integer) stats.get( "labels_removed" ), is( 1 ) );
+ assertThat( (Integer) stats.get( "nodes_created" ), is( 0 ) );
+ assertThat( (Integer) stats.get( "nodes_deleted" ), is( 0 ) );
+ assertThat( (Integer) stats.get( "properties_set" ), is( 0 ) );
+ assertThat( (Integer) stats.get( "relationships_created" ), is( 0 ) );
+ assertThat( (Integer) stats.get( "relationship_deleted" ), is( 0 ) );
+ }
+
+ /**
+ * Ensure that order of data and column is ok.
+ */
+ @Test
+ @Graph( nodes = {
+ @NODE( name = "I", setNameProperty = true ),
+ @NODE( name = "you", setNameProperty = true ),
+ @NODE( name = "him", setNameProperty = true, properties = {
+ @PROP( key = "age", value = "25", type = GraphDescription.PropType.INTEGER ) } ) },
+ relationships = {
+ @REL( start = "I", end = "him", type = "know", properties = { } ),
+ @REL( start = "I", end = "you", type = "know", properties = { } ) } )
+ public void testDataColumnOrder() throws UnsupportedEncodingException {
+ String script = createScript( "MATCH (x)-[r]->(n) WHERE id(x) = %I% RETURN type(r), n.name, n.age" );
+
+ String response = cypherRestCall( script, Status.OK );
+
+ assertThat( response.indexOf( "columns" ) < response.indexOf( "data" ), is( true ));
+ }
+
+ @Test
+ @Title( "Errors" )
+ @Documented( "Errors on the server will be reported as a JSON-formatted message, exception name and stacktrace." )
+ @Graph( "I know you" )
+ public void error_gets_returned_as_json() throws Exception {
+ String response = cypherRestCall( "MATCH (x {name: 'I'}) RETURN x.dummy/0", Status.BAD_REQUEST );
+ Map output = jsonToMap( response );
+ assertTrue( output.toString(), output.containsKey( "message" ) );
+ assertTrue( output.containsKey( "exception" ) );
+ assertTrue( output.containsKey( "stackTrace" ) );
+ }
+
+ @Test
+ @Title( "Return paths" )
+ @Documented( "Paths can be returned just like other return types." )
+ @Graph( "I know you" )
+ public void return_paths() throws Exception {
+ String script = "MATCH path = (x {name: 'I'})--(friend) RETURN path, friend.name";
+ String response = cypherRestCall( script, Status.OK );
+
+ assertEquals( 2, ( jsonToMap( response ) ).size() );
+ assertThat( response, containsString( "data" ) );
+ assertThat( response, containsString( "you" ) );
+ }
+
+ @Test
+ @Title("Use parameters")
+ @Documented( "Cypher supports queries with parameters which are submitted as JSON." )
+ @Graph( value = { "I know you" }, autoIndexNodes = true )
+ public void send_queries_with_parameters() throws Exception {
+ data.get();
+ String script = "MATCH (x {name: {startName}})-[r]-(friend) WHERE friend"
+ + ".name = {name} RETURN TYPE(r)";
+ String response = cypherRestCall( script, Status.OK, Pair.of( "startName", "I" ), Pair.of( "name", "you" ) );
+
+
+ assertEquals( 2, ( jsonToMap( response ) ).size() );
+ assertTrue( response.contains( "know" ) );
+ assertTrue( response.contains( "data" ) );
+ }
+
+ @Test
+ @Documented( "Create a node with a label and a property using Cypher. See the request for the parameter " +
+ "sent with the query." )
+ @Title( "Create a node" )
+ @Graph
+ public void send_query_to_create_a_node() throws Exception {
+ data.get();
+ String script = "CREATE (n:Person { name : {name} }) RETURN n";
+ String response = cypherRestCall( script, Status.OK, Pair.of( "name", "Andres" ) );
+
+ assertTrue( response.contains( "name" ) );
+ assertTrue( response.contains( "Andres" ) );
+ }
+
+ @Test
+ @Title( "Create a node with multiple properties" )
+ @Documented( "Create a node with a label and multiple properties using Cypher. See the request for the parameter " +
+ "sent with the query." )
+ @Graph
+ public void send_query_to_create_a_node_from_a_map() throws Exception
+ {
+ data.get();
+ String script = "CREATE (n:Person { props } ) RETURN n";
+ String params = "\"props\" : { \"position\" : \"Developer\", \"name\" : \"Michael\", \"awesome\" : true, \"children\" : 3 }";
+ String response = cypherRestCall( script, Status.OK, params );
+
+ assertTrue( response.contains( "name" ) );
+ assertTrue( response.contains( "Michael" ) );
+ }
+
+ @Test
+ @Documented( "Create multiple nodes with properties using Cypher. See the request for the parameter sent " +
+ "with the query." )
+ @Title( "Create multiple nodes with properties" )
+ @Graph
+ public void send_query_to_create_multipe_nodes_from_a_map() throws Exception
+ {
+ data.get();
+ String script = "UNWIND {props} AS properties CREATE (n:Person) SET n = properties RETURN n";
+ String params = "\"props\" : [ { \"name\" : \"Andres\", \"position\" : \"Developer\" }, { \"name\" : \"Michael\", \"position\" : \"Developer\" } ]";
+ String response = cypherRestCall( script, Status.OK, params );
+
+ assertTrue( response.contains( "name" ) );
+ assertTrue( response.contains( "Michael" ) );
+ assertTrue( response.contains( "Andres" ) );
+ }
+
+ @Test
+ @Title( "Set all properties on a node using Cypher" )
+ @Documented( "Set all properties on a node." )
+ @Graph
+ public void setAllPropertiesUsingMap() throws Exception
+ {
+ data.get();
+ String script = "CREATE (n:Person { name: 'this property is to be deleted' } ) SET n = { props } RETURN n";
+ String params = "\"props\" : { \"position\" : \"Developer\", \"firstName\" : \"Michael\", \"awesome\" : true, \"children\" : 3 }";
+ String response = cypherRestCall( script, Status.OK, params );
+
+ assertTrue( response.contains( "firstName" ) );
+ assertTrue( response.contains( "Michael" ) );
+ assertTrue( !response.contains( "name" ) );
+ assertTrue( !response.contains( "deleted" ) );
+ }
+
+ @Test
+ @Graph( nodes = {
+ @NODE( name = "I", properties = {
+ @PROP( key = "prop", value = "Hello", type = GraphDescription.PropType.STRING ) } ),
+ @NODE( name = "you" ) },
+ relationships = {
+ @REL( start = "I", end = "him", type = "know", properties = {
+ @PROP( key = "prop", value = "World", type = GraphDescription.PropType.STRING ) } ) } )
+ public void nodes_are_represented_as_nodes() throws Exception {
+ data.get();
+ String script = "MATCH (n)-[r]->() WHERE id(n) = %I% RETURN n, r";
+
+ String response = cypherRestCall( script, Status.OK );
+
+ assertThat( response, containsString( "Hello" ) );
+ assertThat( response, containsString( "World" ) );
+ }
+
+ @Test
+ @Title( "Syntax errors" )
+ @Documented( "Sending a query with syntax errors will give a bad request (HTTP 400) response together with " +
+ "an error message." )
+ @Graph( value = { "I know you" }, autoIndexNodes = true )
+ public void send_queries_with_syntax_errors() throws Exception {
+ data.get();
+ String script = "START x = node:node_auto_index(name={startName}) MATC path = (x-[r]-friend) WHERE friend"
+ + ".name = {name} RETURN TYPE(r)";
+ String response = cypherRestCall( script, Status.BAD_REQUEST, Pair.of( "startName", "I" ), Pair.of( "name", "you" ) );
+
+
+ Map output = jsonToMap( response );
+ assertTrue( output.containsKey( "message" ) );
+ assertTrue( output.containsKey( "stackTrace" ) );
+ }
+
+ @Test
+ @Documented( "When sending queries that\n" +
+ "return nested results like list and maps,\n" +
+ "these will get serialized into nested JSON representations\n" +
+ "according to their types." )
+ @Graph( value = { "I know you" }, autoIndexNodes = true )
+ public void nested_results() throws Exception {
+ data.get();
+ String script = "MATCH (n) WHERE n.name in ['I', 'you'] RETURN collect(n.name)";
+ String response = cypherRestCall(script, Status.OK);System.out.println();
+
+ Map resultMap = jsonToMap( response );
+ assertEquals( 2, resultMap.size() );
+ assertThat( response, anyOf( containsString( "\"I\",\"you\"" ), containsString(
+ "\"you\",\"I\"" ), containsString( "\"I\", \"you\"" ), containsString(
+ "\"you\", \"I\"" )) );
+ }
+
+ @Test
+ @Title( "Profile a query" )
+ @Documented( "By passing in an extra parameter, you can ask the cypher executor to return a profile of the " +
+ "query as it is executed. This can help in locating bottlenecks." )
+ @Graph( nodes = {
+ @NODE( name = "I", setNameProperty = true ),
+ @NODE( name = "you", setNameProperty = true ),
+ @NODE( name = "him", setNameProperty = true, properties = {
+ @PROP( key = "age", value = "25", type = GraphDescription.PropType.INTEGER ) } ) },
+ relationships = {
+ @REL( start = "I", end = "him", type = "know", properties = { } ),
+ @REL( start = "I", end = "you", type = "know", properties = { } ) } )
+ public void testProfiling() throws Exception {
+ String script = createScript( "MATCH (x)-[r]->(n) WHERE id(x) = %I% RETURN type(r), n.name, n.age" );
+
+ // WHEN
+ String response = doCypherRestCall( cypherUri() + "?profile=true", script, Status.OK );
+
+ // THEN
+ Map des = jsonToMap( response );
+ assertThat( des.get( "plan" ), instanceOf( Map.class ));
+
+ @SuppressWarnings("unchecked")
+ Map plan = (Map)des.get( "plan" );
+ assertThat( plan.get( "name" ), instanceOf( String.class ) );
+ assertThat( plan.get( "children" ), instanceOf( Collection.class ));
+ assertThat( plan.get( "rows" ), instanceOf( Number.class ));
+ assertThat( plan.get( "dbHits" ), instanceOf( Number.class ));
+ }
+
+ @Test
+ @Graph( value = { "I know you" }, autoIndexNodes = false )
+ public void array_property() throws Exception {
+ setProperty("I", "array1", new int[] { 1, 2, 3 } );
+ setProperty("I", "array2", new String[] { "a", "b", "c" } );
+
+ String script = "MATCH (n) WHERE id(n) = %I% RETURN n.array1, n.array2";
+ String response = cypherRestCall( script, Status.OK );
+
+ assertThat( response, anyOf( containsString( "[ 1, 2, 3 ]" ), containsString( "[1,2,3]" )) );
+ assertThat( response, anyOf( containsString( "[ \"a\", \"b\", \"c\" ]" ),
+ containsString( "[\"a\",\"b\",\"c\"]" )) );
+ }
+
+ void setProperty(String nodeName, String propertyName, Object propertyValue) {
+ Node i = this.getNode(nodeName);
+ GraphDatabaseService db = i.getGraphDatabase();
+
+ try ( Transaction tx = db.beginTx() )
+ {
+ i.setProperty(propertyName, propertyValue);
+ tx.success();
+ }
+ }
+
+ @Test
+ @Title( "Send queries with errors" )
+ @Documented( "This example shows what happens if you misspell an identifier." )
+ @Graph( value = { "I know you" }, autoIndexNodes = true )
+ public void send_queries_with_errors() throws Exception {
+ data.get();
+ String script = "START x = node:node_auto_index(name={startName}) MATCH path = (x)-[r]-(friend) WHERE frien"
+ + ".name = {name} RETURN type(r)";
+ String response = cypherRestCall( script, Status.BAD_REQUEST, Pair.of( "startName", "I" ), Pair.of( "name", "you" ) );
+
+ Map responseMap = jsonToMap( response );
+ assertThat( responseMap.keySet(), containsInAnyOrder(
+ "message", "exception", "fullname", "stackTrace", "cause", "errors" ) );
+ assertThat( response, containsString( "message" ) );
+ assertThat( ((String) responseMap.get( "message" )), containsString( "Variable `frien` not defined" ) );
+ }
+
+ @SafeVarargs
+ private final String cypherRestCall( String script, Status status, Pair... params )
+ {
+ return doCypherRestCall( cypherUri(), script, status, params );
+ }
+
+ private String cypherRestCall( String script, Status status, String paramString )
+ {
+ return doCypherRestCall( cypherUri(), script, status, paramString );
+ }
+
+ private String cypherUri()
+ {
+ return getDataUri() + "cypher";
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/CypherSessionTest.java b/community/server/src/test/java/org/neo4j/server/rest/CypherSessionTest.java
new file mode 100644
index 0000000000000..a91622d105cbc
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/CypherSessionTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.junit.Test;
+
+import org.neo4j.helpers.collection.Pair;
+import org.neo4j.kernel.configuration.Config;
+import org.neo4j.kernel.impl.factory.GraphDatabaseFacade;
+import org.neo4j.logging.NullLogProvider;
+import org.neo4j.server.database.CypherExecutor;
+import org.neo4j.server.database.Database;
+import org.neo4j.server.database.WrappedDatabase;
+import org.neo4j.server.rest.management.console.CypherSession;
+import org.neo4j.test.TestGraphDatabaseFactory;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class CypherSessionTest
+{
+ @Test
+ public void shouldReturnASingleNode() throws Throwable
+ {
+ GraphDatabaseFacade graphdb = (GraphDatabaseFacade) new TestGraphDatabaseFactory().newImpermanentDatabase();
+ Database database = new WrappedDatabase( graphdb );
+ CypherExecutor executor = new CypherExecutor( database, Config.defaults(), NullLogProvider.getInstance() );
+ executor.start();
+ try
+ {
+ CypherSession session = new CypherSession( executor, NullLogProvider.getInstance(), mock( HttpServletRequest.class ) );
+ Pair result = session.evaluate( "create (a) return a" );
+ assertThat( result.first(), containsString( "Node[0]" ) );
+ }
+ finally
+ {
+ graphdb.shutdown();
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/DatabaseMetadataServiceIT.java b/community/server/src/test/java/org/neo4j/server/rest/DatabaseMetadataServiceIT.java
new file mode 100644
index 0000000000000..4be9786b7b5bc
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/DatabaseMetadataServiceIT.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertThat;
+
+public class DatabaseMetadataServiceIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Documented( "Get relationship types." )
+ @Test
+ public void shouldReturn200OnGet()
+ {
+ helper.createRelationship( "KNOWS" );
+ helper.createRelationship( "LOVES" );
+
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.dataUri() + "relationship/types" )
+ .entity();
+ assertThat( result, allOf( containsString( "KNOWS" ), containsString( "LOVES" ) ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/DegreeIT.java b/community/server/src/test/java/org/neo4j/server/rest/DegreeIT.java
new file mode 100644
index 0000000000000..99d3d7cdd9461
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/DegreeIT.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+
+import static junit.framework.TestCase.assertEquals;
+
+public class DegreeIT extends AbstractRestFunctionalTestBase
+{
+ @Documented( "Get the degree of a node\n" +
+ "\n" +
+ "Return the total number of relationships associated with a node." )
+ @Test
+ @GraphDescription.Graph( {"Root knows Mattias", "Root knows Johan"} )
+ public void get_degree() throws JsonParseException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Root" ) );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .get( nodeUri + "/degree/all" );
+
+ // Then
+ assertEquals( 2, JsonHelper.jsonNode( response.response().getEntity() ).asInt() );
+ }
+
+ @Documented( "Get the degree of a node by direction\n" +
+ "\n" +
+ "Return the number of relationships of a particular direction for a node.\n" +
+ "Specify `all`, `in` or `out`." )
+ @Test
+ @GraphDescription.Graph( {"Root knows Mattias", "Root knows Johan"} )
+ public void get_degree_by_direction() throws JsonParseException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Root" ) );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .get( nodeUri + "/degree/out" );
+
+ // Then
+ assertEquals( 2, JsonHelper.jsonNode( response.response().getEntity() ).asInt() );
+ }
+
+ @Documented( "Get the degree of a node by direction and types\n" +
+ "\n" +
+ "If you are only interested in the degree of a particular relationship type, or a set of relationship types, you specify relationship types after the direction.\n" +
+ "You can combine multiple relationship types by using the `&` character." )
+ @Test
+ @GraphDescription.Graph( {"Root KNOWS Mattias", "Root KNOWS Johan", "Root LIKES Cookie"} )
+ public void get_degree_by_direction_and_type() throws JsonParseException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Root" ) );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .get( nodeUri + "/degree/out/KNOWS&LIKES" );
+
+ // Then
+ assertEquals( 3, JsonHelper.jsonNode( response.response().getEntity() ).asInt() );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/DisableWADLIT.java b/community/server/src/test/java/org/neo4j/server/rest/DisableWADLIT.java
new file mode 100644
index 0000000000000..34a74a106c346
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/DisableWADLIT.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+
+import static org.junit.Assert.assertEquals;
+
+public class DisableWADLIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Test
+ public void should404OnAnyUriEndinginWADL() throws Exception
+ {
+ URI nodeUri = new URI( "http://localhost:7474/db/data/application.wadl" );
+
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( nodeUri );
+
+ httpget.setHeader( "Accept", "*/*" );
+ HttpResponse response = httpclient.execute( httpget );
+
+ assertEquals( 404, response.getStatusLine().getStatusCode() );
+
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/DiscoveryServiceIT.java b/community/server/src/test/java/org/neo4j/server/rest/DiscoveryServiceIT.java
new file mode 100644
index 0000000000000..f5328ee6b6293
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/DiscoveryServiceIT.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+
+import com.sun.jersey.api.client.Client;
+import org.junit.Test;
+
+import org.neo4j.server.rest.domain.JsonHelper;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class DiscoveryServiceIT extends AbstractRestFunctionalTestBase
+{
+ @Test
+ public void shouldRespondWith200WhenRetrievingDiscoveryDocument() throws Exception
+ {
+ JaxRsResponse response = getDiscoveryDocument();
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGetContentLengthHeaderWhenRetrievingDiscoveryDocument() throws Exception
+ {
+ JaxRsResponse response = getDiscoveryDocument();
+ assertNotNull( response.getHeaders()
+ .get( "Content-Length" ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldHaveJsonMediaTypeWhenRetrievingDiscoveryDocument() throws Exception
+ {
+ JaxRsResponse response = getDiscoveryDocument();
+ assertThat( response.getType().toString(), containsString(MediaType.APPLICATION_JSON) );
+ response.close();
+ }
+
+ @Test
+ public void shouldHaveJsonDataInResponse() throws Exception
+ {
+ JaxRsResponse response = getDiscoveryDocument();
+
+ Map map = JsonHelper.jsonToMap( response.getEntity() );
+
+ String managementKey = "management";
+ assertTrue( map.containsKey( managementKey ) );
+ assertNotNull( map.get( managementKey ) );
+
+ String dataKey = "data";
+ assertTrue( map.containsKey( dataKey ) );
+ assertNotNull( map.get( dataKey ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldRedirectOnHtmlRequest() throws Exception
+ {
+ Client nonRedirectingClient = Client.create();
+ nonRedirectingClient.setFollowRedirects( false );
+
+ JaxRsResponse clientResponse = new RestRequest(null,nonRedirectingClient).get(server().baseUri().toString(),MediaType.TEXT_HTML_TYPE);
+
+ assertEquals( 303, clientResponse.getStatus() );
+ }
+
+ private JaxRsResponse getDiscoveryDocument() throws Exception
+ {
+ return new RestRequest(server().baseUri()).get();
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/DocumentationData.java b/community/server/src/test/java/org/neo4j/server/rest/DocumentationData.java
index 83b6ba6a1b86d..c03ae21da99f6 100644
--- a/community/server/src/test/java/org/neo4j/server/rest/DocumentationData.java
+++ b/community/server/src/test/java/org/neo4j/server/rest/DocumentationData.java
@@ -35,7 +35,6 @@ class DocumentationData
public String entity;
public Map requestHeaders;
public Map responseHeaders;
- public boolean ignore;
public void setPayload( final String payload )
{
@@ -56,11 +55,6 @@ public String getPayload()
}
}
- public String getPrettifiedEntity()
- {
- return JSONPrettifier.parse( entity );
- }
-
public void setPayloadType( final MediaType payloadType )
{
this.payloadType = payloadType;
@@ -108,10 +102,6 @@ public void setRequestHeaders( final Map request )
requestHeaders = request;
}
- public void setIgnore() {
- this.ignore = true;
- }
-
@Override
public String toString()
{
@@ -119,4 +109,4 @@ public String toString()
+ ", uri=" + uri + ", method=" + method + ", status=" + status + ", entity=" + entity
+ ", requestHeaders=" + requestHeaders + ", responseHeaders=" + responseHeaders + "]";
}
-}
\ No newline at end of file
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/GetIndexRootIT.java b/community/server/src/test/java/org/neo4j/server/rest/GetIndexRootIT.java
new file mode 100644
index 0000000000000..0c55966ef8de1
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/GetIndexRootIT.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToMap;
+
+public class GetIndexRootIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ /**
+ * /db/data/index is not itself a resource
+ */
+ @Test
+ public void shouldRespondWith404ForNonResourceIndexPath() throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.indexUri() );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ /**
+ * /db/data/index/node should be a resource with no content
+ *
+ * @throws Exception
+ */
+ @Test
+ public void shouldRespondWithNodeIndexes() throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.nodeIndexUri() );
+ assertResponseContainsNoIndexesOtherThanAutoIndexes( response );
+ response.close();
+ }
+
+ private void assertResponseContainsNoIndexesOtherThanAutoIndexes( JaxRsResponse response ) throws JsonParseException
+ {
+ switch ( response.getStatus() )
+ {
+ case 204:
+ return; // OK no auto indices
+ case 200:
+ assertEquals( 0, functionalTestHelper.removeAnyAutoIndex( jsonToMap( response.getEntity() ) ).size() );
+ break;
+ default:
+ fail( "Invalid response code " + response.getStatus() );
+ }
+ }
+
+ /**
+ * /db/data/index/relationship should be a resource with no content
+ *
+ * @throws Exception
+ */
+ @Test
+ public void shouldRespondWithRelationshipIndexes() throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.relationshipIndexUri() );
+ assertResponseContainsNoIndexesOtherThanAutoIndexes( response );
+ response.close();
+ }
+
+ // TODO More tests...
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/GetNodePropertiesIT.java b/community/server/src/test/java/org/neo4j/server/rest/GetNodePropertiesIT.java
new file mode 100644
index 0000000000000..e2c513b843c74
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/GetNodePropertiesIT.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.util.Collections;
+import javax.ws.rs.core.MediaType;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.repr.formats.StreamingJsonFormat;
+import org.neo4j.test.server.HTTP;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import static org.neo4j.helpers.collection.MapUtil.stringMap;
+
+public class GetNodePropertiesIT extends AbstractRestFunctionalDocTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private RestRequest req = RestRequest.req();
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Documented( "Get properties for node." )
+ @Test
+ public void shouldGet200ForProperties() throws JsonParseException {
+ String entity = JsonHelper.createJsonFrom(Collections.singletonMap("foo", "bar"));
+ JaxRsResponse createResponse = req.post(functionalTestHelper.dataUri() + "node/", entity);
+ gen.get()
+ .expectedStatus(200)
+ .get(createResponse.getLocation()
+ .toString() + "/properties");
+ }
+
+ @Test
+ public void shouldGetContentLengthHeaderForRetrievingProperties() throws JsonParseException
+ {
+ String entity = JsonHelper.createJsonFrom(Collections.singletonMap("foo", "bar"));
+ final RestRequest request = req;
+ JaxRsResponse createResponse = request.post(functionalTestHelper.dataUri() + "node/", entity);
+ JaxRsResponse response = request.get(createResponse.getLocation().toString() + "/properties");
+ assertNotNull( response.getHeaders().get("Content-Length") );
+ }
+
+ @Test
+ public void shouldGetCorrectContentEncodingRetrievingProperties() throws JsonParseException
+ {
+ String asianText = "\u4f8b\u5b50";
+ String germanText = "öäüÖÄÜß";
+
+ String complicatedString = asianText + germanText;
+
+
+ String entity = JsonHelper.createJsonFrom( Collections.singletonMap( "foo", complicatedString ));
+ final RestRequest request = req;
+ JaxRsResponse createResponse = request.post(functionalTestHelper.dataUri() + "node/", entity);
+ String response = (String) JsonHelper.readJson( request.get( getPropertyUri( createResponse.getLocation()
+ .toString(), "foo" ) ).getEntity() );
+ assertEquals( complicatedString, response );
+ }
+ @Test
+ public void shouldGetCorrectContentEncodingRetrievingPropertiesWithStreaming() throws JsonParseException
+ {
+ String asianText = "\u4f8b\u5b50";
+ String germanText = "öäüÖÄÜß";
+
+ String complicatedString = asianText + germanText;
+
+ String entity = JsonHelper.createJsonFrom( Collections.singletonMap( "foo", complicatedString ) );
+ final RestRequest request = req.header( StreamingJsonFormat.STREAM_HEADER,"true");
+ JaxRsResponse createResponse = request.post(functionalTestHelper.dataUri() + "node/", entity);
+ String response = (String) JsonHelper.readJson( request.get( getPropertyUri( createResponse.getLocation()
+ .toString(), "foo" ), new MediaType( "application", "json", stringMap( "stream", "true" ) ) ).getEntity() );
+ assertEquals( complicatedString, response );
+ }
+
+ @Test
+ public void shouldGet404ForPropertiesOnNonExistentNode() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.dataUri() + "node/999999/properties");
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void shouldBeJSONContentTypeOnPropertiesResponse() throws JsonParseException
+ {
+ String entity = JsonHelper.createJsonFrom(Collections.singletonMap("foo", "bar"));
+ JaxRsResponse createResource = req.post(functionalTestHelper.dataUri() + "node/", entity);
+ JaxRsResponse response = req.get(createResource.getLocation().toString() + "/properties");
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ }
+
+ @Test
+ public void shouldGet404ForNoProperty()
+ {
+ final JaxRsResponse createResponse = req.post(functionalTestHelper.dataUri() + "node/", "");
+ JaxRsResponse response = req.get(getPropertyUri(createResponse.getLocation().toString(), "foo"));
+ assertEquals(404, response.getStatus());
+ }
+
+ @Documented( "Get property for node.\n" +
+ "\n" +
+ "Get a single node property from a node." )
+ @Test
+ public void shouldGet200ForProperty() throws JsonParseException
+ {
+ String entity = JsonHelper.createJsonFrom(Collections.singletonMap("foo", "bar"));
+ JaxRsResponse createResponse = req.post(functionalTestHelper.dataUri() + "node/", entity);
+ JaxRsResponse response = req.get(getPropertyUri(createResponse.getLocation().toString(), "foo"));
+ assertEquals(200, response.getStatus());
+
+ gen.get()
+ .expectedStatus( 200 )
+ .get(getPropertyUri(createResponse.getLocation()
+ .toString(), "foo"));
+ }
+
+ @Test
+ public void shouldGet404ForPropertyOnNonExistentNode() {
+ JaxRsResponse response = RestRequest.req().get(getPropertyUri(functionalTestHelper.dataUri() + "node/" + "999999", "foo"));
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void shouldBeJSONContentTypeOnPropertyResponse() throws JsonParseException {
+ String entity = JsonHelper.createJsonFrom( Collections.singletonMap( "foo", "bar" ) );
+
+ JaxRsResponse createResponse = req.post(functionalTestHelper.dataUri() + "node/", entity);
+
+ JaxRsResponse response = req.get(getPropertyUri(createResponse.getLocation().toString(), "foo"));
+
+ assertThat( response.getType().toString(), containsString(MediaType.APPLICATION_JSON) );
+
+ createResponse.close();
+ response.close();
+ }
+
+ @Test
+ public void shouldReturnEmptyMapForEmptyProperties() throws Exception
+ {
+ // Given
+ String location = HTTP.POST( server().baseUri().resolve( "db/data/node" ).toString() ).location();
+
+ // When
+ HTTP.Response res = HTTP.GET( location + "/properties" );
+
+ // Then
+ assertThat(res.rawContent(), equalTo("{ }"));
+ }
+
+ private String getPropertyUri( final String baseUri, final String key )
+ {
+ return baseUri + "/properties/" + key;
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/GetOnRootIT.java b/community/server/src/test/java/org/neo4j/server/rest/GetOnRootIT.java
new file mode 100644
index 0000000000000..f50ef612aeb81
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/GetOnRootIT.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.kernel.internal.Version;
+import org.neo4j.server.rest.RESTDocsGenerator.ResponseEntity;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.repr.StreamingFormat;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.TestData.Title;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class GetOnRootIT extends AbstractRestFunctionalTestBase
+{
+ @Title("Get service root")
+ @Documented( "The service root is your starting point to discover the REST API. It contains the basic starting " +
+ "points for the database, and some version and extension information." )
+ @Test
+ @Graph("I know you")
+ public void assert200OkFromGet() throws Exception
+ {
+ String body = gen.get().expectedStatus( 200 ).get( getDataUri() ).entity();
+ Map map = JsonHelper.jsonToMap( body );
+ assertEquals( getDataUri() + "node", map.get( "node" ) );
+ assertNotNull( map.get( "node_index" ) );
+ assertNotNull( map.get( "relationship_index" ) );
+ assertNotNull( map.get( "extensions_info" ) );
+ assertNotNull( map.get( "batch" ) );
+ assertNotNull( map.get( "cypher" ) );
+ assertNotNull( map.get( "indexes" ) );
+ assertNotNull( map.get( "constraints" ) );
+ assertNotNull( map.get( "node_labels" ) );
+ assertEquals( Version.getKernel().getReleaseVersion(), map.get( "neo4j_version" ) );
+
+ // Make sure advertised urls work
+ JaxRsResponse response;
+ if ( map.get( "reference_node" ) != null )
+ {
+ response = RestRequest.req().get(
+ (String) map.get( "reference_node" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+ response = RestRequest.req().get( (String) map.get( "node_index" ) );
+ assertTrue( response.getStatus() == 200 || response.getStatus() == 204 );
+ response.close();
+
+ response = RestRequest.req().get(
+ (String) map.get( "relationship_index" ) );
+ assertTrue( response.getStatus() == 200 || response.getStatus() == 204 );
+ response.close();
+
+ response = RestRequest.req().get( (String) map.get( "extensions_info" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+
+ response = RestRequest.req().post( (String) map.get( "batch" ), "[]" );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+
+ response = RestRequest.req().post( (String) map.get( "cypher" ), "{\"query\":\"CREATE (n) RETURN n\"}" );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+
+ response = RestRequest.req().get( (String) map.get( "indexes" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+
+ response = RestRequest.req().get( (String) map.get( "constraints" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+
+ response = RestRequest.req().get( (String) map.get( "node_labels" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Documented( "All responses from the REST API can be transmitted as JSON streams, resulting in\n" +
+ "better performance and lower memory overhead on the server side. To use\n" +
+ "streaming, supply the header `X-Stream: true` with each request." )
+ @Test
+ public void streaming() throws Exception
+ {
+ data.get();
+ ResponseEntity responseEntity = gen()
+ .withHeader( StreamingFormat.STREAM_HEADER, "true" )
+ .expectedType( APPLICATION_JSON_TYPE )
+ .expectedStatus( 200 )
+ .get( getDataUri() );
+ JaxRsResponse response = responseEntity.response();
+ // this gets the full media type, including things like
+ // ; stream=true at the end
+ String foundMediaType = response.getType()
+ .toString();
+ String expectedMediaType = StreamingFormat.MEDIA_TYPE.toString();
+ assertEquals( expectedMediaType, foundMediaType );
+
+ String body = responseEntity.entity();
+ Map map = JsonHelper.jsonToMap( body );
+ assertEquals( getDataUri() + "node", map.get( "node" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/GetRelationshipPropertiesIT.java b/community/server/src/test/java/org/neo4j/server/rest/GetRelationshipPropertiesIT.java
new file mode 100644
index 0000000000000..4f2c2e8ac72f6
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/GetRelationshipPropertiesIT.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.server.HTTP;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import static org.neo4j.test.server.HTTP.RawPayload.quotedJson;
+
+public class GetRelationshipPropertiesIT extends AbstractRestFunctionalTestBase
+{
+ private static String baseRelationshipUri;
+
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ setupTheDatabase();
+ }
+
+ private static void setupTheDatabase()
+ {
+ long relationship = helper.createRelationship( "LIKES" );
+ Map map = new HashMap();
+ map.put( "foo", "bar" );
+ helper.setRelationshipProperties( relationship, map );
+ baseRelationshipUri = functionalTestHelper.dataUri() + "relationship/" + relationship + "/properties/";
+ }
+
+ @Test
+ public void shouldGet200AndContentLengthForProperties()
+ {
+ long relId = helper.createRelationship( "LIKES" );
+ helper.setRelationshipProperties( relId, Collections.singletonMap( "foo", "bar" ) );
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.dataUri() + "relationship/" + relId
+ + "/properties" );
+ assertEquals( 200, response.getStatus() );
+ assertNotNull( response.getHeaders()
+ .get( "Content-Length" ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet404ForPropertiesOnNonExistentRelationship()
+ {
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.dataUri() +
+ "relationship/999999/properties" );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldBeJSONContentTypeOnPropertiesResponse()
+ {
+ long relId = helper.createRelationship( "LIKES" );
+ helper.setRelationshipProperties( relId, Collections.singletonMap( "foo", "bar" ) );
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.dataUri() + "relationship/" + relId
+ + "/properties" );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ response.close();
+ }
+
+ private String getPropertyUri( String key )
+ {
+ return baseRelationshipUri + key;
+ }
+
+ @Test
+ public void shouldGet404ForNoProperty()
+ {
+ JaxRsResponse response = RestRequest.req().get( getPropertyUri( "baz" ) );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet404ForNonExistingRelationship()
+ {
+ String uri = functionalTestHelper.dataUri() + "relationship/999999/properties/foo";
+ JaxRsResponse response = RestRequest.req().get( uri );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldBeValidJSONOnResponse() throws JsonParseException
+ {
+ JaxRsResponse response = RestRequest.req().get( getPropertyUri( "foo" ) );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ assertNotNull( JsonHelper.createJsonFrom( response.getEntity() ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldReturnEmptyMapForEmptyProperties() throws Exception
+ {
+ // Given
+ String node = HTTP.POST( server().baseUri().resolve( "db/data/node" ).toString() ).location();
+ String rel = HTTP.POST( node + "/relationships", quotedJson( "{'to':'" + node + "', " +
+ "'type':'LOVES'}" ) ).location();
+
+ // When
+ HTTP.Response res = HTTP.GET( rel + "/properties" );
+
+ // Then
+ MatcherAssert.assertThat( res.rawContent(), equalTo( "{ }" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/HtmlIT.java b/community/server/src/test/java/org/neo4j/server/rest/HtmlIT.java
new file mode 100644
index 0000000000000..149f2dadee780
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/HtmlIT.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.util.Collections;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.RelationshipDirection;
+import org.neo4j.test.server.HTTP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class HtmlIT extends AbstractRestFunctionalTestBase
+{
+ private long thomasAnderson;
+ private long trinity;
+ private long thomasAndersonLovesTrinity;
+
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Before
+ public void setupTheDatabase()
+ {
+ // Create the matrix example
+ thomasAnderson = createAndIndexNode( "Thomas Anderson" );
+ trinity = createAndIndexNode( "Trinity" );
+ long tank = createAndIndexNode( "Tank" );
+
+ long knowsRelationshipId = helper.createRelationship( "KNOWS", thomasAnderson, trinity );
+ thomasAndersonLovesTrinity = helper.createRelationship( "LOVES", thomasAnderson, trinity );
+ helper.setRelationshipProperties( thomasAndersonLovesTrinity,
+ Collections.singletonMap( "strength", (Object) 100 ) );
+ helper.createRelationship( "KNOWS", thomasAnderson, tank );
+ helper.createRelationship( "KNOWS", trinity, tank );
+
+ // index a relationship
+ helper.createRelationshipIndex( "relationships" );
+ helper.addRelationshipToIndex( "relationships", "key", "value", knowsRelationshipId );
+
+ // index a relationship
+ helper.createRelationshipIndex( "relationships2" );
+ helper.addRelationshipToIndex( "relationships2", "key2", "value2", knowsRelationshipId );
+ }
+
+ private long createAndIndexNode( String name )
+ {
+ long id = helper.createNode();
+ helper.setNodeProperties( id, Collections.singletonMap( "name", (Object) name ) );
+ helper.addNodeToIndex( "node", "name", name, id );
+ return id;
+ }
+
+ @Test
+ public void shouldGetRoot() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.dataUri(), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ assertValidHtml( response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGetRootWithHTTP() {
+ HTTP.Response response = HTTP.withHeaders("Accept", MediaType.TEXT_HTML).GET(functionalTestHelper.dataUri());
+ assertEquals(Status.OK.getStatusCode(), response.status());
+ assertValidHtml( response.rawContent() );
+ }
+
+ @Test
+ public void shouldGetNodeIndexRoot() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.nodeIndexUri(), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ assertValidHtml( response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGetRelationshipIndexRoot() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.relationshipIndexUri(), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ assertValidHtml( response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGetTrinityWhenSearchingForHer() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.indexNodeUri("node", "name", "Trinity"), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ String entity = response.getEntity();
+ assertTrue(entity.contains("Trinity"));
+ assertValidHtml(entity);
+ response.close();
+ }
+
+ @Test
+ public void shouldGetThomasAndersonDirectly() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.nodeUri(thomasAnderson), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ String entity = response.getEntity();
+ assertTrue(entity.contains("Thomas Anderson"));
+ assertValidHtml(entity);
+ response.close();
+ }
+
+ @Test
+ public void shouldGetSomeRelationships() {
+ final RestRequest request = RestRequest.req();
+ JaxRsResponse response = request.get(functionalTestHelper.relationshipsUri(thomasAnderson, RelationshipDirection.all.name(), "KNOWS"), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ String entity = response.getEntity();
+ assertTrue(entity.contains("KNOWS"));
+ assertFalse(entity.contains("LOVES"));
+ assertValidHtml(entity);
+ response.close();
+
+ response = request.get(functionalTestHelper.relationshipsUri(thomasAnderson, RelationshipDirection.all.name(), "LOVES"),
+ MediaType.TEXT_HTML_TYPE);
+
+ entity = response.getEntity();
+ assertFalse(entity.contains("KNOWS"));
+ assertTrue(entity.contains("LOVES"));
+ assertValidHtml(entity);
+ response.close();
+
+ response = request.get(
+ functionalTestHelper.relationshipsUri(thomasAnderson, RelationshipDirection.all.name(), "LOVES",
+ "KNOWS"),MediaType.TEXT_HTML_TYPE);
+ entity = response.getEntity();
+ assertTrue(entity.contains("KNOWS"));
+ assertTrue(entity.contains("LOVES"));
+ assertValidHtml(entity);
+ response.close();
+ }
+
+ @Test
+ public void shouldGetThomasAndersonLovesTrinityRelationship() {
+ JaxRsResponse response = RestRequest.req().get(functionalTestHelper.relationshipUri(thomasAndersonLovesTrinity), MediaType.TEXT_HTML_TYPE);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ String entity = response.getEntity();
+ assertTrue(entity.contains("strength"));
+ assertTrue(entity.contains("100"));
+ assertTrue(entity.contains("LOVES"));
+ assertValidHtml(entity);
+ response.close();
+ }
+
+ private void assertValidHtml( String entity )
+ {
+ assertTrue( entity.contains( "" ) );
+ assertTrue( entity.contains( "" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/IndexNodeIT.java b/community/server/src/test/java/org/neo4j/server/rest/IndexNodeIT.java
new file mode 100644
index 0000000000000..e9b1e90cc22b5
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/IndexNodeIT.java
@@ -0,0 +1,957 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.function.Factory;
+import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.RESTDocsGenerator.ResponseEntity;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.domain.URIHelper;
+
+import static java.util.Collections.singletonList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.graphdb.Neo4jMatchers.hasProperty;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+import static org.neo4j.server.helpers.FunctionalTestHelper.CLIENT;
+
+public class IndexNodeIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer()
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Before
+ public void setup()
+ {
+ gen().setGraph( server().getDatabase().getGraph() );
+ }
+
+ @Documented( "List node indexes." )
+ @Test
+ public void shouldGetListOfNodeIndexesWhenOneExist() throws JsonParseException
+ {
+ String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+ String entity = gen()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeIndexUri() )
+ .entity();
+
+ Map map = JsonHelper.jsonToMap( entity );
+ assertNotNull( map.get( indexName ) );
+
+ HashMap theIndex = new HashMap<>();
+ theIndex.put( indexName, map.get( indexName ) );
+
+ assertEquals( "Was: " + theIndex + ", no-auto-index:" + functionalTestHelper.removeAnyAutoIndex( theIndex ),
+ 1, functionalTestHelper.removeAnyAutoIndex( theIndex ).size() );
+ }
+
+ @Documented( "Create node index\n" +
+ "\n" +
+ "NOTE: Instead of creating the index this way, you can simply start to use\n" +
+ "it, and it will be created automatically with default configuration." )
+ @Test
+ public void shouldCreateANamedNodeIndex()
+ {
+ String indexName = indexes.newInstance();
+ int expectedIndexes = helper.getNodeIndexes().length + 1;
+ Map indexSpecification = new HashMap<>();
+ indexSpecification.put( "name", indexName );
+
+ gen()
+ .payload( JsonHelper.createJsonFrom( indexSpecification ) )
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .post( functionalTestHelper.nodeIndexUri() );
+
+ assertEquals( expectedIndexes, helper.getNodeIndexes().length );
+ assertThat( helper.getNodeIndexes(), FunctionalTestHelper.arrayContains( indexName ) );
+ }
+
+ @Test
+ public void shouldCreateANamedNodeIndexWithSpaces()
+ {
+ String indexName = indexes.newInstance() + " with spaces";
+ int expectedIndexes = helper.getNodeIndexes().length + 1;
+ Map indexSpecification = new HashMap<>();
+ indexSpecification.put( "name", indexName );
+
+ gen()
+ .payload( JsonHelper.createJsonFrom( indexSpecification ) )
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .post( functionalTestHelper.nodeIndexUri() );
+
+ assertEquals( expectedIndexes, helper.getNodeIndexes().length );
+ assertThat( helper.getNodeIndexes(), FunctionalTestHelper.arrayContains( indexName ) );
+ }
+
+ @Documented( "Create node index with configuration.\n\n" +
+ "This request is only necessary if you want to customize the index settings. \n" +
+ "If you are happy with the defaults, you can just start indexing nodes/relationships, as\n" +
+ "non-existent indexes will automatically be created as you do. See\n" +
+ "<> for more information on index configuration." )
+ @Test
+ public void shouldCreateANamedNodeIndexWithConfiguration() throws Exception
+ {
+ int expectedIndexes = helper.getNodeIndexes().length + 1;
+
+ gen()
+ .payload( "{\"name\":\"fulltext\", \"config\":{\"type\":\"fulltext\",\"provider\":\"lucene\"}}" )
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .post( functionalTestHelper.nodeIndexUri() );
+
+ assertEquals( expectedIndexes, helper.getNodeIndexes().length );
+ assertThat( helper.getNodeIndexes(), FunctionalTestHelper.arrayContains( "fulltext" ) );
+ }
+
+ @Documented( "Add node to index.\n" +
+ "\n" +
+ "Associates a node with the given key/value pair in the given index.\n" +
+ "\n" +
+ "NOTE: Spaces in the URI have to be encoded as +%20+.\n" +
+ "\n" +
+ "CAUTION: This does *not* overwrite previous entries. If you index the\n" +
+ "same key/value/item combination twice, two index entries are created. To\n" +
+ "do update-type operations, you need to delete the old entry before adding\n" +
+ "a new one." )
+ @Test
+ public void shouldAddToIndex() throws Exception
+ {
+ final String indexName = indexes.newInstance();
+ final String key = "some-key";
+ final String value = "some value";
+ long nodeId = createNode();
+ // implicitly create the index
+ gen()
+ .expectedStatus( 201 )
+ .payload(
+ JsonHelper.createJsonFrom( generateNodeIndexCreationPayload( key, value,
+ functionalTestHelper.nodeUri( nodeId ) ) ) )
+ .post( functionalTestHelper.indexNodeUri( indexName ) );
+ // look if we get one entry back
+ JaxRsResponse response = RestRequest.req().get(
+ functionalTestHelper.indexNodeUri( indexName, key,
+ URIHelper.encode( value ) ) );
+ String entity = response.getEntity();
+ Collection> hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 1, hits.size() );
+ }
+
+ @Documented( "Find node by exact match.\n" +
+ "\n" +
+ "NOTE: Spaces in the URI have to be encoded as +%20+." )
+ @Test
+ public void shouldAddToIndexAndRetrieveItByExactMatch() throws Exception
+ {
+ String indexName = indexes.newInstance();
+ String key = "key";
+ String value = "the value";
+ long nodeId = createNode();
+ value = URIHelper.encode( value );
+ // implicitly create the index
+ JaxRsResponse response = RestRequest.req()
+ .post( functionalTestHelper.indexNodeUri( indexName ), createJsonStringFor( nodeId, key, value ) );
+ assertEquals( 201, response.getStatus() );
+
+ // search it exact
+ String entity = gen()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.indexNodeUri( indexName, key, URIHelper.encode( value ) ) )
+ .entity();
+ Collection> hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 1, hits.size() );
+ }
+
+ @Documented( "Find node by query.\n" +
+ "\n" +
+ "The query language used here depends on what type of index you are\n" +
+ "querying. The default index type is Lucene, in which case you should use\n" +
+ "the Lucene query language here. Below an example of a fuzzy search over\n" +
+ "multiple keys.\n" +
+ "\n" +
+ "See: {lucene-base-uri}/queryparser/org/apache/lucene/queryparser/classic/package-summary.html\n" +
+ "\n" +
+ "Getting the results with a predefined ordering requires adding the\n" +
+ "parameter\n" +
+ "\n" +
+ "`order=ordering`\n" +
+ "\n" +
+ "where ordering is one of index, relevance or score. In this case an\n" +
+ "additional field will be added to each result, named score, that holds\n" +
+ "the float value that is the score reported by the query result." )
+ @Test
+ public void shouldAddToIndexAndRetrieveItByQuery() throws JsonParseException
+ {
+ String indexName = indexes.newInstance();
+ String key = "Name";
+ String value = "Builder";
+ long node = helper.createNode( MapUtil.map( key, value ) );
+ helper.addNodeToIndex( indexName, key, value, node );
+ helper.addNodeToIndex( indexName, "Gender", "Male", node );
+
+ String entity = gen()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.indexNodeUri( indexName ) + "?query=" + key +
+ ":Build~0.1%20AND%20Gender:Male" )
+ .entity();
+
+ Collection> hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 1, hits.size() );
+ LinkedHashMap nodeMap = (LinkedHashMap) hits.iterator().next();
+ assertNull( "score should not be present when not explicitly ordering", nodeMap.get( "score" ) );
+ }
+
+ @Test
+ public void orderedResultsAreSupersetOfUnordered() throws Exception
+ {
+ // Given
+ String indexName = indexes.newInstance();
+ String key = "Name";
+ String value = "Builder";
+ long node = helper.createNode( MapUtil.map( key, value ) );
+ helper.addNodeToIndex( indexName, key, value, node );
+ helper.addNodeToIndex( indexName, "Gender", "Male", node );
+
+ String entity = gen().expectedStatus( 200 ).get(
+ functionalTestHelper.indexNodeUri( indexName )
+ + "?query=" + key + ":Build~0.1%20AND%20Gender:Male" ).entity();
+
+ @SuppressWarnings( "unchecked" )
+ Collection> hits =
+ (Collection>) JsonHelper.readJson( entity );
+ LinkedHashMap nodeMapUnordered = hits.iterator().next();
+
+ // When
+ entity = gen().expectedStatus( 200 ).get(
+ functionalTestHelper.indexNodeUri( indexName )
+ + "?query="+key+":Build~0.1%20AND%20Gender:Male&order=score" ).entity();
+
+ //noinspection unchecked
+ hits = (Collection>) JsonHelper.readJson( entity );
+ LinkedHashMap nodeMapOrdered = hits.iterator().next();
+
+ // Then
+ for ( Map.Entry unorderedEntry : nodeMapUnordered.entrySet() )
+ {
+ assertEquals( "wrong entry for key: " + unorderedEntry.getKey(),
+ unorderedEntry.getValue(),
+ nodeMapOrdered.get( unorderedEntry.getKey() ) );
+ }
+ assertTrue( "There should be only one extra value for the ordered map",
+ nodeMapOrdered.size() == nodeMapUnordered.size() + 1 );
+ }
+
+ //TODO:add compatibility tests for old syntax
+ @Test
+ public void shouldAddToIndexAndRetrieveItByQuerySorted()
+ throws JsonParseException
+ {
+ String indexName = indexes.newInstance();
+ String key = "Name";
+ long node1 = helper.createNode();
+ long node2 = helper.createNode();
+
+ helper.addNodeToIndex( indexName, key, "Builder2", node1 );
+ helper.addNodeToIndex( indexName, "Gender", "Male", node1 );
+ helper.addNodeToIndex( indexName, key, "Builder", node2 );
+ helper.addNodeToIndex( indexName, "Gender", "Male", node2 );
+
+ String entity = gen().expectedStatus( 200 ).get(
+ functionalTestHelper.indexNodeUri( indexName )
+ + "?query=" + key + ":Builder~%20AND%20Gender:Male&order=relevance" ).entity();
+
+ Collection> hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 2, hits.size() );
+ @SuppressWarnings( "unchecked" )
+ Iterator> it = (Iterator>) hits.iterator();
+
+ LinkedHashMap node2Map = it.next();
+ LinkedHashMap node1Map = it.next();
+ float score2 = ( (Double) node2Map.get( "score" ) ).floatValue();
+ float score1 = ( (Double) node1Map.get( "score" ) ).floatValue();
+ assertTrue(
+ "results returned in wrong order for relevance ordering",
+ ( (String) node2Map.get( "self" ) ).endsWith( Long.toString( node2 ) ) );
+ assertTrue(
+ "results returned in wrong order for relevance ordering",
+ ( (String) node1Map.get( "self" ) ).endsWith( Long.toString( node1 ) ) );
+ /*
+ * scores are always the same, just the ordering changes. So all subsequent tests will
+ * check the same condition.
+ */
+ assertTrue( "scores are reversed", score2 > score1 );
+
+ entity = gen().expectedStatus( 200 ).get(
+ functionalTestHelper.indexNodeUri( indexName )
+ + "?query="+key+":Builder~%20AND%20Gender:Male&order=index" ).entity();
+
+ hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 2, hits.size() );
+ //noinspection unchecked
+ it = (Iterator>) hits.iterator();
+
+ /*
+ * index order, so as they were added
+ */
+ node1Map = it.next();
+ node2Map = it.next();
+ score1 = ( (Double) node1Map.get( "score" ) ).floatValue();
+ score2 = ( (Double) node2Map.get( "score" ) ).floatValue();
+ assertTrue(
+ "results returned in wrong order for index ordering",
+ ( (String) node1Map.get( "self" ) ).endsWith( Long.toString( node1 ) ) );
+ assertTrue(
+ "results returned in wrong order for index ordering",
+ ( (String) node2Map.get( "self" ) ).endsWith( Long.toString( node2 ) ) );
+ assertTrue( "scores are reversed", score2 > score1 );
+
+ entity = gen().expectedStatus( 200 ).get(
+ functionalTestHelper.indexNodeUri( indexName )
+ + "?query="+key+":Builder~%20AND%20Gender:Male&order=score" ).entity();
+
+ hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 2, hits.size() );
+ //noinspection unchecked
+ it = (Iterator>) hits.iterator();
+
+ node2Map = it.next();
+ node1Map = it.next();
+ score2 = ( (Double) node2Map.get( "score" ) ).floatValue();
+ score1 = ( (Double) node1Map.get( "score" ) ).floatValue();
+ assertTrue(
+ "results returned in wrong order for score ordering",
+ ( (String) node2Map.get( "self" ) ).endsWith( Long.toString( node2 ) ) );
+ assertTrue(
+ "results returned in wrong order for score ordering",
+ ( (String) node1Map.get( "self" ) ).endsWith( Long.toString( node1 ) ) );
+ assertTrue( "scores are reversed", score2 > score1 );
+ }
+
+ /**
+ * POST ${org.neo4j.server.rest.web}/index/node/{indexName}/{key}/{value}
+ * "http://uri.for.node.to.index"
+ */
+ @Test
+ public void shouldRespondWith201CreatedWhenIndexingJsonNodeUri()
+ {
+ final long nodeId = helper.createNode();
+ final String key = "key";
+ final String value = "value";
+ final String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+
+ JaxRsResponse response = RestRequest.req()
+ .post( functionalTestHelper.indexNodeUri( indexName ), createJsonStringFor( nodeId, key, value ) );
+ assertEquals( 201, response.getStatus() );
+ assertNotNull( response.getHeaders()
+ .getFirst( "Location" ) );
+ assertEquals( singletonList( nodeId ), helper.getIndexedNodes( indexName, key, value ) );
+ }
+
+ @Test
+ public void shouldGetNodeRepresentationFromIndexUri() throws JsonParseException
+ {
+ long nodeId = helper.createNode();
+ String key = "key2";
+ String value = "value";
+
+ String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+ JaxRsResponse response = RestRequest.req()
+ .post( functionalTestHelper.indexNodeUri( indexName ),
+ createJsonStringFor( nodeId, key, value ));
+
+ assertEquals( Status.CREATED.getStatusCode(), response.getStatus() );
+ String indexUri = response.getHeaders()
+ .getFirst( "Location" );
+
+ response = RestRequest.req()
+ .get( indexUri );
+ assertEquals( 200, response.getStatus() );
+
+ String entity = response.getEntity();
+
+ Map map = JsonHelper.jsonToMap( entity );
+ assertNotNull( map.get( "self" ) );
+ }
+
+ @Test
+ public void shouldGet404WhenRequestingIndexUriWhichDoesntExist()
+ {
+ String key = "key3";
+ String value = "value";
+ String indexName = indexes.newInstance();
+ String indexUri = functionalTestHelper.nodeIndexUri() + indexName + "/" + key + "/" + value;
+ JaxRsResponse response = RestRequest.req()
+ .get( indexUri );
+ assertEquals( Status.NOT_FOUND.getStatusCode(), response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet404WhenDeletingNonExtistentIndex()
+ {
+ final String indexName = indexes.newInstance();
+ String indexUri = functionalTestHelper.nodeIndexUri() + indexName;
+ JaxRsResponse response = RestRequest.req().delete( indexUri );
+ assertEquals( Status.NOT_FOUND.getStatusCode(), response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet200AndArrayOfNodeRepsWhenGettingFromIndex() throws JsonParseException
+ {
+ String key = "myKey";
+ String value = "myValue";
+
+ String name1 = "Thomas Anderson";
+ String name2 = "Agent Smith";
+
+ String indexName = indexes.newInstance();
+ final RestRequest request = RestRequest.req();
+ JaxRsResponse responseToPost = request.post( functionalTestHelper.nodeUri(), "{\"name\":\"" + name1 + "\"}" );
+ assertEquals( 201, responseToPost.getStatus() );
+ String location1 = responseToPost.getHeaders()
+ .getFirst( HttpHeaders.LOCATION );
+ responseToPost.close();
+ responseToPost = request.post( functionalTestHelper.nodeUri(), "{\"name\":\"" + name2 + "\"}" );
+ assertEquals( 201, responseToPost.getStatus() );
+ String location2 = responseToPost.getHeaders()
+ .getFirst( HttpHeaders.LOCATION );
+ responseToPost.close();
+ responseToPost = request.post( functionalTestHelper.indexNodeUri( indexName ),
+ createJsonStringFor( functionalTestHelper.getNodeIdFromUri( location1 ), key, value ) );
+ assertEquals( 201, responseToPost.getStatus() );
+ String indexLocation1 = responseToPost.getHeaders()
+ .getFirst( HttpHeaders.LOCATION );
+ responseToPost.close();
+ responseToPost = request.post( functionalTestHelper.indexNodeUri( indexName ),
+ createJsonStringFor( functionalTestHelper.getNodeIdFromUri( location2 ), key, value ) );
+ assertEquals( 201, responseToPost.getStatus() );
+ String indexLocation2 = responseToPost.getHeaders()
+ .getFirst( HttpHeaders.LOCATION );
+ Map uriToName = new HashMap<>();
+ uriToName.put( indexLocation1, name1 );
+ uriToName.put( indexLocation2, name2 );
+ responseToPost.close();
+
+ JaxRsResponse response = RestRequest.req()
+ .get( functionalTestHelper.indexNodeUri( indexName, key, value ) );
+ assertEquals( 200, response.getStatus() );
+ Collection> items = (Collection>) JsonHelper.readJson( response.getEntity() );
+ int counter = 0;
+ for ( Object item : items )
+ {
+ Map, ?> map = (Map, ?>) item;
+ Map, ?> properties = (Map, ?>) map.get( "data" );
+ assertNotNull( map.get( "self" ) );
+ String indexedUri = (String) map.get( "indexed" );
+ assertEquals( uriToName.get( indexedUri ), properties.get( "name" ) );
+ counter++;
+ }
+ assertEquals( 2, counter );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet200WhenGettingNodesFromIndexWithNoHits()
+ {
+ final String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+ JaxRsResponse response = RestRequest.req()
+ .get( functionalTestHelper.indexNodeUri( indexName, "non-existent-key", "non-existent-value" ) );
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Documented( "Delete node index." )
+ @Test
+ public void shouldReturn204WhenRemovingNodeIndexes()
+ {
+ final String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+
+ gen()
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.indexNodeUri( indexName ) );
+ }
+
+ //
+ // REMOVING ENTRIES
+ //
+
+ @Documented( "Remove all entries with a given node from an index." )
+ @Test
+ public void shouldBeAbleToRemoveIndexingById()
+ {
+ String key1 = "kvkey1";
+ String key2 = "kvkey2";
+ String value1 = "value1";
+ String value2 = "value2";
+ String indexName = indexes.newInstance();
+ long node = helper.createNode( MapUtil.map( key1, value1, key1, value2, key2, value1, key2, value2 ) );
+ helper.addNodeToIndex( indexName, key1, value1, node );
+ helper.addNodeToIndex( indexName, key1, value2, node );
+ helper.addNodeToIndex( indexName, key2, value1, node );
+ helper.addNodeToIndex( indexName, key2, value2, node );
+
+ gen()
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.indexNodeUri( indexName ) + "/" + node );
+
+ assertEquals( 0, helper.getIndexedNodes( indexName, key1, value1 )
+ .size() );
+ assertEquals( 0, helper.getIndexedNodes( indexName, key1, value2 )
+ .size() );
+ assertEquals( 0, helper.getIndexedNodes( indexName, key2, value1 )
+ .size() );
+ assertEquals( 0, helper.getIndexedNodes( indexName, key2, value2 )
+ .size() );
+ }
+
+ @Documented( "Remove all entries with a given node and key from an index." )
+ @Test
+ public void shouldBeAbleToRemoveIndexingByIdAndKey()
+ {
+ String key1 = "kvkey1";
+ String key2 = "kvkey2";
+ String value1 = "value1";
+ String value2 = "value2";
+ String indexName = indexes.newInstance();
+ long node = helper.createNode( MapUtil.map( key1, value1, key1, value2, key2, value1, key2, value2 ) );
+ helper.addNodeToIndex( indexName, key1, value1, node );
+ helper.addNodeToIndex( indexName, key1, value2, node );
+ helper.addNodeToIndex( indexName, key2, value1, node );
+ helper.addNodeToIndex( indexName, key2, value2, node );
+
+ gen()
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.nodeIndexUri() + indexName + "/" + key2 + "/" + node );
+
+ assertEquals( 1, helper.getIndexedNodes( indexName, key1, value1 )
+ .size() );
+ assertEquals( 1, helper.getIndexedNodes( indexName, key1, value2 )
+ .size() );
+ assertEquals( 0, helper.getIndexedNodes( indexName, key2, value1 )
+ .size() );
+ assertEquals( 0, helper.getIndexedNodes( indexName, key2, value2 )
+ .size() );
+ }
+
+ @Documented( "Remove all entries with a given node, key and value from an index." )
+ @Test
+ public void shouldBeAbleToRemoveIndexingByIdAndKeyAndValue()
+ {
+ String key1 = "kvkey1";
+ String key2 = "kvkey2";
+ String value1 = "value1";
+ String value2 = "value2";
+ String indexName = indexes.newInstance();
+ long node = helper.createNode( MapUtil.map( key1, value1, key1, value2, key2, value1, key2, value2 ) );
+ helper.addNodeToIndex( indexName, key1, value1, node );
+ helper.addNodeToIndex( indexName, key1, value2, node );
+ helper.addNodeToIndex( indexName, key2, value1, node );
+ helper.addNodeToIndex( indexName, key2, value2, node );
+
+ gen()
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.nodeIndexUri() + indexName + "/" + key1 + "/" + value1 + "/" + node );
+
+ assertEquals( 0, helper.getIndexedNodes( indexName, key1, value1 )
+ .size() );
+ assertEquals( 1, helper.getIndexedNodes( indexName, key1, value2 )
+ .size() );
+ assertEquals( 1, helper.getIndexedNodes( indexName, key2, value1 )
+ .size() );
+ assertEquals( 1, helper.getIndexedNodes( indexName, key2, value2 )
+ .size() );
+
+ }
+
+ @Test
+ public void shouldBeAbleToIndexValuesContainingSpaces() throws Exception
+ {
+ final long nodeId = helper.createNode();
+ final String key = "key";
+ final String value = "value with spaces in it";
+
+ String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+ final RestRequest request = RestRequest.req();
+ JaxRsResponse response = request.post( functionalTestHelper.indexNodeUri( indexName ),
+ createJsonStringFor( nodeId, key, value ) );
+
+ assertEquals( Status.CREATED.getStatusCode(), response.getStatus() );
+ URI location = response.getLocation();
+ response.close();
+ response = request.get( functionalTestHelper.indexNodeUri( indexName, key, URIHelper.encode( value ) ) );
+ assertEquals( Status.OK.getStatusCode(), response.getStatus() );
+ Collection> hits = (Collection>) JsonHelper.readJson( response.getEntity() );
+ assertEquals( 1, hits.size() );
+ response.close();
+
+ CLIENT.resource( location )
+ .delete();
+ response = request.get( functionalTestHelper.indexNodeUri( indexName, key, URIHelper.encode( value ) ) );
+ hits = (Collection>) JsonHelper.readJson( response.getEntity() );
+ assertEquals( 0, hits.size() );
+ }
+
+ @Test
+ public void shouldRespondWith400WhenSendingCorruptJson() throws Exception
+ {
+ final String indexName = indexes.newInstance();
+ helper.createNodeIndex( indexName );
+ final String corruptJson = "{\"key\" \"myKey\"}";
+ JaxRsResponse response = RestRequest.req()
+ .post( functionalTestHelper.indexNodeUri( indexName ),
+ corruptJson );
+ assertEquals( 400, response.getStatus() );
+ response.close();
+ }
+
+ @Documented( "Get or create unique node (create).\n" +
+ "\n" +
+ "The node is created if it doesn't exist in the unique index already." )
+ @Test
+ public void get_or_create_a_node_in_an_unique_index() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Tobias";
+ helper.createNodeIndex( index );
+ ResponseEntity response = gen()
+ .expectedStatus( 201 /* created */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value
+ + "\", \"properties\": {\"" + key + "\": \"" + value
+ + "\", \"sequence\": 1}}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?uniqueness=get_or_create" );
+
+ MultivaluedMap headers = response.response().getHeaders();
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ assertEquals( result.get( "indexed" ), headers.getFirst( "Location" ) );
+ Map data = assertCast( Map.class, result.get( "data" ) );
+ assertEquals( value, data.get( key ) );
+ assertEquals( 1, data.get( "sequence" ) );
+ }
+
+ @Test
+ public void get_or_create_node_with_array_properties() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Tobias";
+ helper.createNodeIndex( index );
+ ResponseEntity response = gen()
+ .expectedStatus( 201 /* created */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value
+ + "\", \"properties\": {\"" + key + "\": \"" + value
+ + "\", \"array\": [1,2,3]}}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?unique" );
+
+ MultivaluedMap headers = response.response().getHeaders();
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ String location = headers.getFirst("Location");
+ assertEquals( result.get( "indexed" ), location );
+ Map data = assertCast( Map.class, result.get( "data" ) );
+ assertEquals( value, data.get( key ) );
+ assertEquals(Arrays.asList( 1, 2, 3), data.get( "array" ) );
+ Node node;
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ node = graphdb().index().forNodes(index).get(key, value).getSingle();
+ }
+ assertThat( node, inTx( graphdb(), hasProperty( key ).withValue( value ) ) );
+ assertThat( node, inTx( graphdb(), hasProperty( "array" ).withValue( new int[]{1, 2, 3} ) ) );
+ }
+
+ @Documented( "Get or create unique node (existing).\n" +
+ "\n" +
+ "Here,\n" +
+ "a node is not created but the existing unique node returned, since another node\n" +
+ "is indexed with the same data already. The node data returned is then that of the\n" +
+ "already existing node." )
+ @Test
+ public void get_or_create_unique_node_if_already_existing() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+
+ GraphDatabaseService graphdb = graphdb();
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ Node peter = graphdb.createNode();
+ peter.setProperty( key, value );
+ peter.setProperty( "sequence", 1 );
+ graphdb.index().forNodes( index ).add( peter, key, value );
+
+ tx.success();
+ }
+
+ helper.createNodeIndex( index );
+ ResponseEntity response = gen()
+ .expectedStatus( 200 /* ok */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value
+ + "\", \"properties\": {\"" + key + "\": \"" + value
+ + "\", \"sequence\": 2}}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?uniqueness=get_or_create" );
+
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ Map data = assertCast( Map.class, result.get( "data" ) );
+ assertEquals( value, data.get( key ) );
+ assertEquals( 1, data.get( "sequence" ) );
+ }
+
+ @Documented( "Create a unique node or return fail (create).\n" +
+ "\n" +
+ "Here, in case\n" +
+ "of an already existing node, an error should be returned. In this\n" +
+ "example, no existing indexed node is found and a new node is created." )
+ @Test
+ public void create_a_unique_node_or_fail_create() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Tobias";
+ helper.createNodeIndex( index );
+ ResponseEntity response = gen.get()
+ .expectedStatus( 201 /* created */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value
+ + "\", \"properties\": {\"" + key + "\": \"" + value
+ + "\", \"sequence\": 1}}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?uniqueness=create_or_fail" +
+ "" );
+
+ MultivaluedMap headers = response.response().getHeaders();
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ assertEquals( result.get( "indexed" ), headers.getFirst( "Location" ) );
+ Map data = assertCast( Map.class, result.get( "data" ) );
+ assertEquals( value, data.get( key ) );
+ assertEquals( 1, data.get( "sequence" ) );
+ }
+
+
+ @Documented( "Create a unique node or return fail (fail).\n" +
+ "\n" +
+ "Here, in case\n" +
+ "of an already existing node, an error should be returned. In this\n" +
+ "example, an existing node indexed with the same data\n" +
+ "is found and an error is returned." )
+ @Test
+ public void create_a_unique_node_or_return_fail___fail() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+
+ GraphDatabaseService graphdb = graphdb();
+ helper.createNodeIndex( index );
+
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node peter = graphdb.createNode();
+ peter.setProperty( key, value );
+ peter.setProperty( "sequence", 1 );
+ graphdb.index().forNodes( index ).add( peter, key, value );
+
+ tx.success();
+ }
+
+ RestRequest.req();
+
+ ResponseEntity response = gen.get()
+ .expectedStatus( 409 /* conflict */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value
+ + "\", \"properties\": {\"" + key + "\": \"" + value
+ + "\", \"sequence\": 2}}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?uniqueness=create_or_fail" );
+
+
+
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ Map data = assertCast( Map.class, result.get( "data" ) );
+ assertEquals( value, data.get( key ) );
+ assertEquals( 1, data.get( "sequence" ) );
+ }
+
+ @Documented( "Add an existing node to unique index (not indexed).\n" +
+ "\n" +
+ "Associates a node with the given key/value pair in the given unique\n" +
+ "index.\n" +
+ "\n" +
+ "In this example, we are using `create_or_fail` uniqueness." )
+ @Test
+ public void addExistingNodeToUniqueIndexAdded() throws Exception
+ {
+ final String indexName = indexes.newInstance();
+ final String key = "some-key";
+ final String value = "some value";
+ long nodeId = createNode();
+ // implicitly create the index
+ gen()
+ .expectedStatus( 201 /* created */ )
+ .payload(
+ JsonHelper.createJsonFrom( generateNodeIndexCreationPayload( key, value,
+ functionalTestHelper.nodeUri( nodeId ) ) ) )
+ .post( functionalTestHelper.indexNodeUri( indexName ) + "?uniqueness=create_or_fail" );
+ // look if we get one entry back
+ JaxRsResponse response = RestRequest.req()
+ .get( functionalTestHelper.indexNodeUri( indexName, key, URIHelper.encode( value ) ) );
+ String entity = response.getEntity();
+ Collection> hits = (Collection>) JsonHelper.readJson( entity );
+ assertEquals( 1, hits.size() );
+ }
+
+ @Documented( "Add an existing node to unique index (already indexed).\n" +
+ "\n" +
+ "In this case, the node already exists in the index, and thus we get a `HTTP 409` status response,\n" +
+ "as we have set the uniqueness to `create_or_fail`." )
+ @Test
+ public void addExistingNodeToUniqueIndexExisting() throws Exception
+ {
+ final String indexName = indexes.newInstance();
+ final String key = "some-key";
+ final String value = "some value";
+
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ Node peter = graphdb().createNode();
+ peter.setProperty( key, value );
+ graphdb().index().forNodes( indexName ).add( peter, key, value );
+
+ tx.success();
+ }
+
+ gen()
+ .expectedStatus( 409 /* conflict */ )
+ .payload(
+ JsonHelper.createJsonFrom( generateNodeIndexCreationPayload( key, value,
+ functionalTestHelper.nodeUri( createNode() ) ) ) )
+ .post( functionalTestHelper.indexNodeUri( indexName ) + "?uniqueness=create_or_fail" );
+ }
+
+ @Documented( "Backward Compatibility Test (using old syntax ?unique)\n" +
+ "Put node if absent - Create.\n" +
+ "\n" +
+ "Add a node to an index unless a node already exists for the given index data. In\n" +
+ "this case, a new node is created since nothing existing is found in the index." )
+ @Test
+ public void put_node_if_absent___create() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Mattias";
+ helper.createNodeIndex( index );
+ String uri = functionalTestHelper.nodeIndexUri() + index + "?unique";
+ gen().expectedStatus( 201 /* created */ )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\":\"" + functionalTestHelper.nodeUri( helper.createNode() ) + "\"}" )
+ .post( uri );
+ }
+
+ @Test
+ public void already_indexed_node_should_not_fail_on_create_or_fail() throws Exception
+ {
+ // Given
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createNodeIndex( index );
+ Node node;
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ node = graphdb.createNode();
+ graphdb.index().forNodes( index ).add( node, key, value );
+ tx.success();
+ }
+
+ // When & Then
+ gen.get()
+ .expectedStatus( 201 )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\":\""
+ + functionalTestHelper.nodeUri( node.getId() ) + "\"}" )
+ .post( functionalTestHelper.nodeIndexUri() + index + "?uniqueness=create_or_fail" );
+ }
+
+ private static T assertCast( Class type, Object object )
+ {
+ assertTrue( type.isInstance( object ) );
+ return type.cast( object );
+ }
+
+ private long createNode()
+ {
+ GraphDatabaseService graphdb = server().getDatabase().getGraph();
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node = graphdb.createNode();
+ tx.success();
+ return node.getId();
+ }
+ }
+
+ private String createJsonStringFor( final long nodeId, final String key, final String value )
+ {
+ return "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\": \""
+ + functionalTestHelper.nodeUri( nodeId ) + "\"}";
+ }
+
+ private Object generateNodeIndexCreationPayload( String key, String value, String nodeUri )
+ {
+ Map results = new HashMap<>();
+ results.put( "key", key );
+ results.put( "value", value );
+ results.put( "uri", nodeUri );
+ return results;
+ }
+
+ private final Factory indexes = UniqueStrings.withPrefix( "index" );
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/IndexRelationshipIT.java b/community/server/src/test/java/org/neo4j/server/rest/IndexRelationshipIT.java
new file mode 100644
index 0000000000000..090869ed0fe5a
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/IndexRelationshipIT.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.function.Factory;
+import org.neo4j.graphdb.GraphDatabaseService;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Relationship;
+import org.neo4j.graphdb.RelationshipType;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.RESTDocsGenerator.ResponseEntity;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.domain.URIHelper;
+
+import static java.util.Arrays.asList;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import static org.neo4j.server.helpers.FunctionalTestHelper.CLIENT;
+
+public class IndexRelationshipIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+ private static RestRequest request;
+
+ private enum MyRelationshipTypes implements RelationshipType
+ {
+ KNOWS
+ }
+
+ @BeforeClass
+ public static void setupServer()
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ request = RestRequest.req();
+ }
+
+ /**
+ * POST ${org.neo4j.server.rest.web}/index/relationship {
+ * "name":"index-name" "config":{ // optional map of index configuration
+ * params "key1":"value1", "key2":"value2" } }
+ *
+ * POST ${org.neo4j.server.rest.web}/index/relationship/{indexName}/{key}/{
+ * value} "http://uri.for.node.to.index"
+ */
+ @Test
+ public void shouldCreateANamedRelationshipIndexAndAddToIt() throws JsonParseException
+ {
+ String indexName = indexes.newInstance();
+ int expectedIndexes = helper.getRelationshipIndexes().length + 1;
+ Map indexSpecification = new HashMap<>();
+ indexSpecification.put( "name", indexName );
+ JaxRsResponse response = httpPostIndexRelationshipRoot( JsonHelper.createJsonFrom( indexSpecification ) );
+ assertEquals( 201, response.getStatus() );
+ assertNotNull( response.getHeaders().get( "Location" ).get( 0 ) );
+ assertEquals( expectedIndexes, helper.getRelationshipIndexes().length );
+ assertNotNull( helper.createRelationshipIndex( indexName ) );
+ // Add a relationship to the index
+ String key = "key";
+ String value = "value";
+ String relationshipType = "related-to";
+ long relationshipId = helper.createRelationship( relationshipType );
+ response = httpPostIndexRelationshipNameKeyValue( indexName, relationshipId, key, value );
+ assertEquals( Status.CREATED.getStatusCode(), response.getStatus() );
+ String indexUri = response.getHeaders().get( "Location" ).get( 0 );
+ assertNotNull( indexUri );
+ assertEquals( Arrays.asList( (Long) relationshipId ), helper.getIndexedRelationships( indexName, key, value ) );
+ // Get the relationship from the indexed URI (Location in header)
+ response = httpGet( indexUri );
+ assertEquals( 200, response.getStatus() );
+ String discovredEntity = response.getEntity();
+ Map map = JsonHelper.jsonToMap( discovredEntity );
+ assertNotNull( map.get( "self" ) );
+ }
+
+ @Test
+ public void shouldGet404WhenRequestingIndexUriWhichDoesntExist()
+ {
+ String key = "key3";
+ String value = "value";
+ String indexName = indexes.newInstance();
+ String indexUri = functionalTestHelper.relationshipIndexUri() + indexName + "/" + key + "/" + value;
+ JaxRsResponse response = httpGet( indexUri );
+ assertEquals( Status.NOT_FOUND.getStatusCode(), response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet404WhenDeletingNonExtistentIndex()
+ {
+ String indexName = indexes.newInstance();
+ String indexUri = functionalTestHelper.relationshipIndexUri() + indexName;
+ JaxRsResponse response = request.delete( indexUri );
+ assertEquals( Status.NOT_FOUND.getStatusCode(), response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet200AndArrayOfRelationshipRepsWhenGettingFromIndex() throws JsonParseException
+ {
+ final long startNode = helper.createNode();
+ final long endNode = helper.createNode();
+ final String key = "key_get";
+ final String value = "value";
+ final String relationshipName1 = "related-to";
+ final String relationshipName2 = "dislikes";
+ String jsonString = jsonRelationshipCreationSpecification( relationshipName1, endNode, key, value );
+ JaxRsResponse createRelationshipResponse = httpPostCreateRelationship( startNode, jsonString );
+ assertEquals( 201, createRelationshipResponse.getStatus() );
+ String relationshipLocation1 = createRelationshipResponse.getLocation().toString();
+ jsonString = jsonRelationshipCreationSpecification( relationshipName2, endNode, key, value );
+ createRelationshipResponse = httpPostCreateRelationship( startNode, jsonString );
+ assertEquals( 201, createRelationshipResponse.getStatus() );
+ String relationshipLocation2 = createRelationshipResponse.getHeaders().get( HttpHeaders.LOCATION ).get( 0 );
+ String indexName = indexes.newInstance();
+ JaxRsResponse indexCreationResponse = httpPostIndexRelationshipRoot( "{\"name\":\"" + indexName + "\"}" );
+ assertEquals( 201, indexCreationResponse.getStatus() );
+ JaxRsResponse indexedRelationshipResponse = httpPostIndexRelationshipNameKeyValue( indexName,
+ functionalTestHelper.getRelationshipIdFromUri( relationshipLocation1 ), key, value );
+ String indexLocation1 = indexedRelationshipResponse.getHeaders().get( HttpHeaders.LOCATION ).get( 0 );
+ indexedRelationshipResponse = httpPostIndexRelationshipNameKeyValue( indexName,
+ functionalTestHelper.getRelationshipIdFromUri( relationshipLocation2 ), key, value );
+ String indexLocation2 = indexedRelationshipResponse.getHeaders().get( HttpHeaders.LOCATION ).get( 0 );
+ Map uriToName = new HashMap<>();
+ uriToName.put( indexLocation1.toString(), relationshipName1 );
+ uriToName.put( indexLocation2.toString(), relationshipName2 );
+ JaxRsResponse response = RestRequest.req().get(
+ functionalTestHelper.indexRelationshipUri( indexName, key, value ) );
+ assertEquals( 200, response.getStatus() );
+ Collection> items = (Collection>) JsonHelper.readJson( response.getEntity() );
+ int counter = 0;
+ for ( Object item : items )
+ {
+ Map, ?> map = (Map, ?>) item;
+ assertNotNull( map.get( "self" ) );
+ String indexedUri = (String) map.get( "indexed" );
+ assertEquals( uriToName.get( indexedUri ), map.get( "type" ) );
+ counter++;
+ }
+ assertEquals( 2, counter );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet200WhenGettingRelationshipFromIndexWithNoHits()
+ {
+ String indexName = indexes.newInstance();
+ helper.createRelationshipIndex( indexName );
+ JaxRsResponse response = RestRequest.req().get(
+ functionalTestHelper.indexRelationshipUri( indexName, "non-existent-key", "non-existent-value" ) );
+ assertEquals( 200, response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet200WhenQueryingIndex()
+ {
+ String indexName = indexes.newInstance();
+ String key = "bobsKey";
+ String value = "bobsValue";
+ long relationship = helper.createRelationship( "TYPE" );
+ helper.addRelationshipToIndex( indexName, key, value, relationship );
+ JaxRsResponse response = RestRequest.req().get(
+ functionalTestHelper.indexRelationshipUri( indexName ) + "?query=" + key + ":" + value );
+ assertEquals( 200, response.getStatus() );
+ }
+
+ @Test
+ public void shouldBeAbleToRemoveIndexing()
+ {
+ String key1 = "kvkey1";
+ String key2 = "kvkey2";
+ String value1 = "value1";
+ String value2 = "value2";
+ String indexName = indexes.newInstance();
+ long relationship = helper.createRelationship( "some type" );
+ helper.setRelationshipProperties( relationship,
+ MapUtil.map( key1, value1, key1, value2, key2, value1, key2, value2 ) );
+ helper.addRelationshipToIndex( indexName, key1, value1, relationship );
+ helper.addRelationshipToIndex( indexName, key1, value2, relationship );
+ helper.addRelationshipToIndex( indexName, key2, value1, relationship );
+ helper.addRelationshipToIndex( indexName, key2, value2, relationship );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key1, value1 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key1, value2 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key2, value1 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key2, value2 ).size() );
+ JaxRsResponse response = RestRequest.req().delete(
+ functionalTestHelper.relationshipIndexUri() + indexName + "/" + key1 + "/" + value1 + "/"
+ + relationship );
+ assertEquals( 204, response.getStatus() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key1, value1 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key1, value2 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key2, value1 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key2, value2 ).size() );
+ response = RestRequest.req().delete(
+ functionalTestHelper.relationshipIndexUri() + indexName + "/" + key2 + "/" + relationship );
+ assertEquals( 204, response.getStatus() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key1, value1 ).size() );
+ assertEquals( 1, helper.getIndexedRelationships( indexName, key1, value2 ).size() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key2, value1 ).size() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key2, value2 ).size() );
+ response = RestRequest.req().delete(
+ functionalTestHelper.relationshipIndexUri() + indexName + "/" + relationship );
+ assertEquals( 204, response.getStatus() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key1, value1 ).size() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key1, value2 ).size() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key2, value1 ).size() );
+ assertEquals( 0, helper.getIndexedRelationships( indexName, key2, value2 ).size() );
+ // Delete the index
+ response = RestRequest.req().delete( functionalTestHelper.indexRelationshipUri( indexName ) );
+ assertEquals( 204, response.getStatus() );
+ assertFalse( asList( helper.getRelationshipIndexes() ).contains( indexName ) );
+ }
+
+ @Test
+ public void shouldBeAbleToIndexValuesContainingSpaces() throws Exception
+ {
+ final long startNodeId = helper.createNode();
+ final long endNodeId = helper.createNode();
+ final String relationshiptype = "tested-together";
+ final long relationshipId = helper.createRelationship( relationshiptype, startNodeId, endNodeId );
+ final String key = "key";
+ final String value = "value with spaces in it";
+ final String indexName = indexes.newInstance();
+ helper.createRelationshipIndex( indexName );
+ JaxRsResponse response = httpPostIndexRelationshipNameKeyValue( indexName, relationshipId, key, value );
+ assertEquals( Status.CREATED.getStatusCode(), response.getStatus() );
+ URI location = response.getLocation();
+ response.close();
+ response = httpGetIndexRelationshipNameKeyValue( indexName, key, URIHelper.encode( value ) );
+ assertEquals( Status.OK.getStatusCode(), response.getStatus() );
+ String responseEntity = response.getEntity();
+ Collection> hits = (Collection>) JsonHelper.readJson( responseEntity );
+ assertEquals( 1, hits.size() );
+ response.close();
+ CLIENT.resource( location ).delete();
+ response = httpGetIndexRelationshipNameKeyValue( indexName, key, URIHelper.encode( value ) );
+ assertEquals( 200, response.getStatus() );
+ responseEntity = response.getEntity();
+ hits = (Collection>) JsonHelper.readJson( responseEntity );
+ assertEquals( 0, hits.size() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith400WhenSendingCorruptJson() throws Exception
+ {
+ final String indexName = indexes.newInstance();
+ helper.createRelationshipIndex( indexName );
+ final String corruptJson = "{[}";
+ JaxRsResponse response = RestRequest.req().post( functionalTestHelper.indexRelationshipUri( indexName ),
+ corruptJson );
+ assertEquals( 400, response.getStatus() );
+ }
+
+ @Documented( "Get or create unique relationship (create).\n" +
+ "\n" +
+ "Create a unique relationship in an index.\n" +
+ "If a relationship matching the given key and value already exists in the index, it will be returned.\n" +
+ "If not, a new relationship will be created.\n" +
+ "\n" +
+ "NOTE: The type and direction of the relationship is not regarded when determining uniqueness." )
+ @Test
+ public void get_or_create_relationship() throws Exception
+ {
+ final String index = indexes.newInstance(), type="knowledge", key = "name", value = "Tobias";
+ helper.createRelationshipIndex( index );
+ long start = helper.createNode();
+ long end = helper.createNode();
+ gen.get()
+ .expectedStatus( 201 /* created */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload( "{\"key\": \"" + key + "\", \"value\":\"" + value +
+ "\", \"start\": \"" + functionalTestHelper.nodeUri( start ) +
+ "\", \"end\": \"" + functionalTestHelper.nodeUri( end ) +
+ "\", \"type\": \"" + type + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "/?uniqueness=get_or_create" );
+ }
+
+ @Documented( "Get or create unique relationship (existing).\n" +
+ "\n" +
+ "Here, in case\n" +
+ "of an already existing relationship, the sent data is ignored and the\n" +
+ "existing relationship returned." )
+ @Test
+ public void get_or_create_unique_relationship_existing() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createRelationshipIndex( index );
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ Relationship rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ graphdb.index().forRelationships( index ).add( rel, key, value );
+ tx.success();
+ }
+ gen.get()
+ .expectedStatus( 200 /* existing */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"start\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"end\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"type\":\""
+ + MyRelationshipTypes.KNOWS + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=get_or_create" );
+ }
+
+ @Documented( "Create a unique relationship or return fail (create).\n" +
+ "\n" +
+ "Here, in case\n" +
+ "of an already existing relationship, an error should be returned. In this\n" +
+ "example, no existing relationship is found and a new relationship is created." )
+ @Test
+ public void create_a_unique_relationship_or_return_fail___create() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Tobias";
+ helper.createRelationshipIndex( index );
+ ResponseEntity response = gen
+ .get()
+ .expectedStatus( 201 /* created */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"start\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"end\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"type\":\""
+ + MyRelationshipTypes.KNOWS + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=create_or_fail" );
+ MultivaluedMap headers = response.response().getHeaders();
+ Map result = JsonHelper.jsonToMap( response.entity() );
+ assertEquals( result.get( "indexed" ), headers.getFirst( "Location" ) );
+ }
+
+ @Documented( "Create a unique relationship or return fail (fail).\n" +
+ "\n" +
+ "Here, in case\n" +
+ "of an already existing relationship, an error should be returned. In this\n" +
+ "example, an existing relationship is found and an error is returned." )
+ @Test
+ public void create_a_unique_relationship_or_return_fail___fail() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createRelationshipIndex( index );
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ Relationship rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ graphdb.index().forRelationships( index ).add( rel, key, value );
+ tx.success();
+ }
+ gen.get()
+ .expectedStatus( 409 /* conflict */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"start\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"end\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"type\":\""
+ + MyRelationshipTypes.KNOWS + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=create_or_fail" );
+ }
+
+ @Documented( "Add an existing relationship to a unique index (not indexed).\n" +
+ "\n" +
+ "If a relationship matching the given key and value already exists in the index, it will be returned.\n" +
+ "If not, an `HTTP 409` (conflict) status will be returned in this case, as we are using `create_or_fail`.\n" +
+ "\n" +
+ "It's possible to use `get_or_create` uniqueness as well.\n" +
+ "\n" +
+ "NOTE: The type and direction of the relationship is not regarded when determining uniqueness." )
+ @Test
+ public void put_relationship_or_fail_if_absent() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ helper.createRelationshipIndex( index );
+ gen.get()
+ .expectedStatus( 201 /* created */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \""
+ + key
+ + "\", \"value\": \""
+ + value
+ + "\", \"uri\":\""
+ + functionalTestHelper.relationshipUri( helper.createRelationship( "KNOWS",
+ helper.createNode(), helper.createNode() ) ) + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=create_or_fail" );
+ }
+
+ @Documented( "Add an existing relationship to a unique index (already indexed)." )
+ @Test
+ public void put_relationship_if_absent_only_fail() throws Exception
+ {
+ // Given
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createRelationshipIndex( index );
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ Relationship rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ graphdb.index().forRelationships( index ).add( rel, key, value );
+ tx.success();
+ }
+
+ Relationship rel;
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ tx.success();
+ }
+
+ // When & Then
+ gen.get()
+ .expectedStatus( 409 /* conflict */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\":\""
+ + functionalTestHelper.relationshipUri( rel.getId() ) + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=create_or_fail" );
+ }
+
+ @Test
+ public void already_indexed_relationship_should_not_fail_on_create_or_fail() throws Exception
+ {
+ // Given
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createRelationshipIndex( index );
+ Relationship rel;
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ graphdb.index().forRelationships( index ).add( rel, key, value );
+ tx.success();
+ }
+
+ // When & Then
+ gen.get()
+ .expectedStatus( 201 )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\":\""
+ + functionalTestHelper.relationshipUri( rel.getId() ) + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?uniqueness=create_or_fail" );
+ }
+
+ /**
+ * This can be safely removed in version 1.11 an onwards.
+ */
+ @Test
+ public void createUniqueShouldBeBackwardsCompatibleWith1_8() throws Exception
+ {
+ final String index = indexes.newInstance(), key = "name", value = "Peter";
+ GraphDatabaseService graphdb = graphdb();
+ helper.createRelationshipIndex( index );
+ try ( Transaction tx = graphdb.beginTx() )
+ {
+ Node node1 = graphdb.createNode();
+ Node node2 = graphdb.createNode();
+ Relationship rel = node1.createRelationshipTo( node2, MyRelationshipTypes.KNOWS );
+ graphdb.index().forRelationships( index ).add( rel, key, value );
+ tx.success();
+ }
+ gen.get()
+ .expectedStatus( 200 /* conflict */)
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .payload(
+ "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"start\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"end\": \""
+ + functionalTestHelper.nodeUri( helper.createNode() ) + "\", \"type\":\""
+ + MyRelationshipTypes.KNOWS + "\"}" )
+ .post( functionalTestHelper.relationshipIndexUri() + index + "?unique" );
+ }
+
+ private JaxRsResponse httpPostIndexRelationshipRoot( String jsonIndexSpecification )
+ {
+ return RestRequest.req().post( functionalTestHelper.relationshipIndexUri(), jsonIndexSpecification );
+ }
+
+ private JaxRsResponse httpGetIndexRelationshipNameKeyValue( String indexName, String key, String value )
+ {
+ return RestRequest.req().get( functionalTestHelper.indexRelationshipUri( indexName, key, value ) );
+ }
+
+ private JaxRsResponse httpPostIndexRelationshipNameKeyValue( String indexName, long relationshipId, String key,
+ String value )
+ {
+ return RestRequest.req().post( functionalTestHelper.indexRelationshipUri( indexName ),
+ createJsonStringFor( relationshipId, key, value ) );
+ }
+
+ private String createJsonStringFor( final long relationshipId, final String key, final String value )
+ {
+ return "{\"key\": \"" + key + "\", \"value\": \"" + value + "\", \"uri\": \""
+ + functionalTestHelper.relationshipUri( relationshipId ) + "\"}";
+ }
+
+ private JaxRsResponse httpGet( String indexUri )
+ {
+ return request.get( indexUri );
+ }
+
+ private JaxRsResponse httpPostCreateRelationship( long startNode, String jsonString )
+ {
+ return RestRequest.req().post( functionalTestHelper.dataUri() + "node/" + startNode + "/relationships",
+ jsonString );
+ }
+
+ private String jsonRelationshipCreationSpecification( String relationshipName, long endNode, String key,
+ String value )
+ {
+ return "{\"to\" : \"" + functionalTestHelper.dataUri() + "node/" + endNode + "\"," + "\"type\" : \""
+ + relationshipName + "\", " + "\"data\" : {\"" + key + "\" : \"" + value + "\"}}";
+ }
+
+ private final Factory indexes = UniqueStrings.withPrefix( "index" );
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/JmxServiceIT.java b/community/server/src/test/java/org/neo4j/server/rest/JmxServiceIT.java
new file mode 100644
index 0000000000000..4cf60cebc7dbb
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/JmxServiceIT.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+
+public class JmxServiceIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Test
+ public void shouldRespondWithJMXResources() throws Exception {
+ String url = functionalTestHelper.managementUri() + "/server/jmx";
+ JaxRsResponse resp = RestRequest.req().get(url);
+ String json = resp.getEntity();
+
+ assertEquals(json, 200, resp.getStatus());
+ assertThat(json, containsString("resources"));
+ assertThat(json, containsString("jmx/domain/{domain}/{objectName}"));
+ resp.close();
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/LabelsIT.java b/community/server/src/test/java/org/neo4j/server/rest/LabelsIT.java
new file mode 100644
index 0000000000000..baef2c9b49cb6
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/LabelsIT.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.helpers.collection.Iterables;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.web.PropertyValueException;
+import org.neo4j.test.GraphDescription;
+import org.neo4j.test.GraphDescription.LABEL;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+
+import static java.util.Arrays.asList;
+
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.graphdb.Label.label;
+import static org.neo4j.graphdb.Neo4jMatchers.hasLabel;
+import static org.neo4j.graphdb.Neo4jMatchers.hasLabels;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+import static org.neo4j.helpers.collection.Iterables.map;
+import static org.neo4j.helpers.collection.Iterators.asSet;
+import static org.neo4j.server.rest.domain.JsonHelper.createJsonFrom;
+import static org.neo4j.server.rest.domain.JsonHelper.readJson;
+import static org.neo4j.test.GraphDescription.PropType.ARRAY;
+import static org.neo4j.test.GraphDescription.PropType.STRING;
+
+public class LabelsIT extends AbstractRestFunctionalTestBase
+{
+
+ @Documented( "Adding a label to a node." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true ) } )
+ public void adding_a_label_to_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Clint Eastwood" ) );
+
+ gen.get()
+ .description( startGraph( "adding a label to a node" ) )
+ .expectedStatus( 204 )
+ .payload( createJsonFrom( "Person" ) )
+ .post( nodeUri + "/labels" );
+ }
+
+ @Documented( "Adding multiple labels to a node." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true ) } )
+ public void adding_multiple_labels_to_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Clint Eastwood" ) );
+
+ gen.get()
+ .description( startGraph( "adding multiple labels to a node" ) )
+ .expectedStatus( 204 )
+ .payload( createJsonFrom( new String[]{"Person", "Actor"} ) )
+ .post( nodeUri + "/labels" );
+
+ // Then
+ assertThat( nodes.get( "Clint Eastwood" ), inTx( graphdb(), hasLabels( "Person", "Actor" ) ) );
+ }
+
+ @Documented( "Adding a label with an invalid name.\n" +
+ "\n" +
+ "Labels with empty names are not allowed, however, all other valid strings are accepted as label names.\n" +
+ "Adding an invalid label to a node will lead to a HTTP 400 response." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true ) } )
+ public void adding_an_invalid_label_to_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Clint Eastwood" ) );
+
+ gen.get()
+ .expectedStatus( 400 )
+ .payload( createJsonFrom( "" ) )
+ .post( nodeUri + "/labels" );
+ }
+
+ @Documented( "Replacing labels on a node.\n" +
+ "\n" +
+ "This removes any labels currently on a node, and replaces them with the labels passed in as the\n" +
+ "request body." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true,
+ labels = { @LABEL( "Person" ) }) } )
+ public void replacing_labels_on_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Clint Eastwood" ) );
+
+ // When
+ gen.get()
+ .description( startGraph( "replacing labels on a node" ) )
+ .expectedStatus( 204 )
+ .payload( createJsonFrom( new String[]{"Actor", "Director"}) )
+ .put( nodeUri + "/labels" );
+
+ // Then
+ assertThat( nodes.get( "Clint Eastwood" ), inTx(graphdb(), hasLabels("Actor", "Director")) );
+ }
+
+ @Documented( "Listing labels for a node." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", labels = { @LABEL( "Actor" ), @LABEL( "Director" ) }, setNameProperty = true ) } )
+ public void listing_node_labels() throws JsonParseException
+ {
+ Map nodes = data.get();
+ String nodeUri = getNodeUri( nodes.get( "Clint Eastwood" ) );
+
+ String body = gen.get()
+ .expectedStatus( 200 )
+ .get( nodeUri + "/labels" )
+ .entity();
+ @SuppressWarnings("unchecked")
+ List labels = (List) readJson( body );
+ assertEquals( asSet( "Actor", "Director" ), Iterables.asSet( labels ) );
+ }
+
+ @Documented( "Removing a label from a node." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true, labels = { @LABEL( "Person" ) } ) } )
+ public void removing_a_label_from_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ Node node = nodes.get( "Clint Eastwood" );
+ String nodeUri = getNodeUri( node );
+
+ String labelName = "Person";
+ gen.get()
+ .description( startGraph( "removing a label from a node" ) )
+ .expectedStatus( 204 )
+ .delete( nodeUri + "/labels/" + labelName );
+
+ assertThat( node, inTx( graphdb(), not( hasLabel( label( labelName ) ) ) ) );
+ }
+
+ @Documented( "Removing a non-existent label from a node." )
+ @Test
+ @GraphDescription.Graph( nodes = { @NODE( name = "Clint Eastwood", setNameProperty = true ) } )
+ public void removing_a_non_existent_label_from_a_node() throws PropertyValueException
+ {
+ Map nodes = data.get();
+ Node node = nodes.get( "Clint Eastwood" );
+ String nodeUri = getNodeUri( node );
+
+ String labelName = "Person";
+ gen.get()
+ .description( startGraph( "removing a non-existent label from a node" ) )
+ .expectedStatus( 204 )
+ .delete( nodeUri + "/labels/" + labelName );
+
+ assertThat( node, inTx( graphdb(), not( hasLabel( label( labelName ) ) ) ) );
+ }
+
+ @Documented( "Get all nodes with a label." )
+ @Test
+ @GraphDescription.Graph( nodes = {
+ @NODE( name = "Clint Eastwood", setNameProperty = true, labels = { @LABEL( "Actor" ), @LABEL( "Director" ) } ),
+ @NODE( name = "Donald Sutherland", setNameProperty = true, labels = { @LABEL( "Actor" ) } ),
+ @NODE( name = "Steven Spielberg", setNameProperty = true, labels = { @LABEL( "Director" ) } )
+ } )
+ public void get_all_nodes_with_label() throws JsonParseException
+ {
+ data.get();
+ String uri = getNodesWithLabelUri( "Actor" );
+ String body = gen.get()
+ .expectedStatus( 200 )
+ .get( uri )
+ .entity();
+
+ List> parsed = (List>) readJson( body );
+ assertEquals( asSet( "Clint Eastwood", "Donald Sutherland" ), Iterables
+ .asSet( map( getProperty( "name", String.class ), parsed ) ) );
+ }
+
+ @Test
+ @Documented( "Get nodes by label and property.\n" +
+ "\n" +
+ "You can retrieve all nodes with a given label and property by passing one property as a query parameter.\n" +
+ "Notice that the property value is JSON-encoded and then URL-encoded.\n" +
+ "\n" +
+ "If there is an index available on the label/property combination you send, that index will be used. If no\n" +
+ "index is available, all nodes with the given label will be filtered through to find matching nodes.\n" +
+ "\n" +
+ "Currently, it is not possible to search using multiple properties." )
+ @GraphDescription.Graph( nodes = {
+ @NODE( name = "Donald Sutherland", labels={ @LABEL( "Person" )} ),
+ @NODE( name = "Clint Eastwood", labels={ @LABEL( "Person" )}, properties = { @PROP( key = "name", value = "Clint Eastwood" )}),
+ @NODE( name = "Steven Spielberg", labels={ @LABEL( "Person" )}, properties = { @PROP( key = "name", value = "Steven Spielberg" )})})
+ public void get_nodes_with_label_and_property() throws JsonParseException, UnsupportedEncodingException
+ {
+ data.get();
+
+ String labelName = "Person";
+
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .get( getNodesWithLabelAndPropertyUri( labelName, "name", "Clint Eastwood" ) )
+ .entity();
+
+ List> parsed = (List>) readJson( result );
+ assertEquals( asSet( "Clint Eastwood" ), Iterables.asSet( map( getProperty( "name", String.class ), parsed ) ) );
+ }
+
+ @Test
+ @Documented( "Get nodes by label and array property." )
+ @GraphDescription.Graph( nodes = {
+ @NODE(name = "Donald Sutherland", labels = {@LABEL("Person")}),
+ @NODE(name = "Clint Eastwood", labels = {@LABEL("Person")}, properties =
+ {@PROP(key = "names", value = "Clint,Eastwood", type = ARRAY, componentType = STRING)}),
+ @NODE(name = "Steven Spielberg", labels = {@LABEL("Person")}, properties =
+ {@PROP(key = "names", value = "Steven,Spielberg", type = ARRAY, componentType = STRING)})})
+ public void get_nodes_with_label_and_array_property() throws JsonParseException, UnsupportedEncodingException
+ {
+ data.get();
+
+ String labelName = "Person";
+
+ String uri = getNodesWithLabelAndPropertyUri( labelName, "names", new String[] { "Clint", "Eastwood" } );
+
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .get( uri )
+ .entity();
+
+ List> parsed = (List>) readJson( result );
+ assertEquals( 1, parsed.size() );
+
+ //noinspection AssertEqualsBetweenInconvertibleTypes
+ assertEquals( Iterables.asSet( asList( asList( "Clint", "Eastwood" ) ) ),
+ Iterables.asSet( map( getProperty( "names", List.class ), parsed ) ) );
+ }
+
+ @Test
+ @Documented( "List all labels.\n" +
+ " \n" +
+ "By default, the server will return labels in use only. If you also want to return labels not in use,\n" +
+ "append the \"in_use=0\" query parameter." )
+ @GraphDescription.Graph( nodes = {
+ @NODE( name = "Clint Eastwood", setNameProperty = true, labels = { @LABEL( "Person" ), @LABEL( "Actor" ), @LABEL( "Director" ) } ),
+ @NODE( name = "Donald Sutherland", setNameProperty = true, labels = { @LABEL( "Person" ), @LABEL( "Actor" ) } ),
+ @NODE( name = "Steven Spielberg", setNameProperty = true, labels = { @LABEL( "Person" ), @LABEL( "Director" ) } )
+ } )
+ public void list_all_labels() throws JsonParseException
+ {
+ data.get();
+ String uri = getLabelsUri();
+ String body = gen.get()
+ .expectedStatus( 200 )
+ .get( uri )
+ .entity();
+
+ Set> parsed = Iterables.asSet((List>) readJson( body ));
+ assertTrue( parsed.contains( "Person" ) );
+ assertTrue( parsed.contains( "Actor" ) );
+ assertTrue( parsed.contains( "Director" ) );
+ }
+
+ private Function getProperty( final String propertyKey, final Class propertyType )
+ {
+ return from -> {
+ Map, ?> node = (Map, ?>) from;
+ Map, ?> data1 = (Map, ?>) node.get( "data" );
+ return propertyType.cast( data1.get( propertyKey ) );
+ };
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/ListPropertyKeysIT.java b/community/server/src/test/java/org/neo4j/server/rest/ListPropertyKeysIT.java
new file mode 100644
index 0000000000000..9985e39d1aebb
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/ListPropertyKeysIT.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Test;
+
+import org.neo4j.helpers.collection.Iterables;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.server.rest.domain.JsonHelper.readJson;
+
+public class ListPropertyKeysIT extends AbstractRestFunctionalTestBase
+{
+ @Test
+ @Documented( "List all property keys." )
+ @GraphDescription.Graph( nodes = {
+ @GraphDescription.NODE( name = "a", setNameProperty = true ),
+ @GraphDescription.NODE( name = "b", setNameProperty = true ),
+ @GraphDescription.NODE( name = "c", setNameProperty = true )
+ } )
+ public void list_all_property_keys_ever_used() throws JsonParseException
+ {
+ data.get();
+ String uri = getPropertyKeysUri();
+ String body = gen.get()
+ .expectedStatus( 200 )
+ .get( uri )
+ .entity();
+
+ Set> parsed = Iterables.asSet((List>) readJson( body ));
+ assertTrue( parsed.contains( "name" ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/ManageNodeIT.java b/community/server/src/test/java/org/neo4j/server/rest/ManageNodeIT.java
new file mode 100644
index 0000000000000..302526dc75f65
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/ManageNodeIT.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.hamcrest.MatcherAssert;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.neo4j.helpers.FakeClock;
+import org.neo4j.kernel.GraphDatabaseDependencies;
+import org.neo4j.kernel.configuration.Config;
+import org.neo4j.kernel.configuration.Settings;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.kernel.impl.factory.GraphDatabaseFacade;
+import org.neo4j.kernel.internal.KernelData;
+import org.neo4j.kernel.monitoring.Monitors;
+import org.neo4j.logging.LogProvider;
+import org.neo4j.logging.NullLogProvider;
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.NeoServer;
+import org.neo4j.server.configuration.ServerSettings;
+import org.neo4j.server.database.Database;
+import org.neo4j.server.database.WrappedDatabase;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.management.JmxService;
+import org.neo4j.server.rest.management.RootService;
+import org.neo4j.server.rest.management.VersionAndEditionService;
+import org.neo4j.server.rest.management.console.ConsoleService;
+import org.neo4j.server.rest.management.console.ConsoleSessionFactory;
+import org.neo4j.server.rest.management.console.ScriptSession;
+import org.neo4j.server.rest.management.console.ShellSession;
+import org.neo4j.server.rest.repr.OutputFormat;
+import org.neo4j.server.rest.repr.formats.JsonFormat;
+import org.neo4j.shell.ShellSettings;
+import org.neo4j.string.UTF8;
+import org.neo4j.test.TestData;
+import org.neo4j.test.TestGraphDatabaseFactory;
+import org.neo4j.test.server.EntityOutputFormat;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+import org.neo4j.test.server.HTTP;
+
+import static java.lang.System.lineSeparator;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import static org.neo4j.helpers.collection.MapUtil.stringMap;
+import static org.neo4j.server.configuration.ServerSettings.httpConnector;
+import static org.neo4j.test.SuppressOutput.suppressAll;
+
+public class ManageNodeIT extends AbstractRestFunctionalDocTestBase
+{
+ private static final long NON_EXISTENT_NODE_ID = 999999;
+ private static String NODE_URI_PATTERN = "^.*/node/[0-9]+$";
+
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Test
+ public void create_node() throws Exception
+ {
+ JaxRsResponse response = gen.get()
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .post( functionalTestHelper.nodeUri() )
+ .response();
+ assertTrue( response.getLocation()
+ .toString()
+ .matches( NODE_URI_PATTERN ) );
+ }
+
+ @Test
+ public void create_node_with_properties() throws Exception
+ {
+ JaxRsResponse response = gen.get()
+ .payload( "{\"foo\" : \"bar\"}" )
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .expectedHeader( "Content-Length" )
+ .post( functionalTestHelper.nodeUri() )
+ .response();
+ assertTrue( response.getLocation()
+ .toString()
+ .matches( NODE_URI_PATTERN ) );
+ }
+
+ @Test
+ public void create_node_with_array_properties() throws Exception
+ {
+ String response = gen.get()
+ .payload( "{\"foo\" : [1,2,3]}" )
+ .expectedStatus( 201 )
+ .expectedHeader( "Location" )
+ .expectedHeader( "Content-Length" )
+ .post( functionalTestHelper.nodeUri() )
+ .response().getEntity();
+ assertThat( response, containsString( "[ 1, 2, 3 ]" ) );
+ }
+
+ @Documented( "Property values can not be null.\n" +
+ "\n" +
+ "This example shows the response you get when trying to set a property to +null+." )
+ @Test
+ public void shouldGet400WhenSupplyingNullValueForAProperty() throws Exception
+ {
+ gen.get()
+ .payload( "{\"foo\":null}" )
+ .expectedStatus( 400 )
+ .post( functionalTestHelper.nodeUri() );
+ }
+
+ @Test
+ public void shouldGet400WhenCreatingNodeMalformedProperties() throws Exception
+ {
+ JaxRsResponse response = sendCreateRequestToServer("this:::isNot::JSON}");
+ assertEquals( 400, response.getStatus() );
+ }
+
+ @Test
+ public void shouldGet400WhenCreatingNodeUnsupportedNestedPropertyValues() throws Exception
+ {
+ JaxRsResponse response = sendCreateRequestToServer("{\"foo\" : {\"bar\" : \"baz\"}}");
+ assertEquals( 400, response.getStatus() );
+ }
+
+ private JaxRsResponse sendCreateRequestToServer(final String json)
+ {
+ return RestRequest.req().post( functionalTestHelper.dataUri() + "node/" , json );
+ }
+
+ private JaxRsResponse sendCreateRequestToServer()
+ {
+ return RestRequest.req().post( functionalTestHelper.dataUri() + "node/" , null, MediaType.APPLICATION_JSON_TYPE );
+ }
+
+ @Test
+ public void shouldGetValidLocationHeaderWhenCreatingNode() throws Exception
+ {
+ JaxRsResponse response = sendCreateRequestToServer();
+ assertNotNull( response.getLocation() );
+ assertTrue( response.getLocation()
+ .toString()
+ .startsWith( functionalTestHelper.dataUri() + "node/" ) );
+ }
+
+ @Test
+ public void shouldGetASingleContentLengthHeaderWhenCreatingANode()
+ {
+ JaxRsResponse response = sendCreateRequestToServer();
+ List contentLentgthHeaders = response.getHeaders()
+ .get( "Content-Length" );
+ assertNotNull( contentLentgthHeaders );
+ assertEquals( 1, contentLentgthHeaders.size() );
+ }
+
+ @Test
+ public void shouldBeJSONContentTypeOnResponse()
+ {
+ JaxRsResponse response = sendCreateRequestToServer();
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ }
+
+ @Test
+ public void shouldGetValidNodeRepresentationWhenCreatingNode() throws Exception
+ {
+ JaxRsResponse response = sendCreateRequestToServer();
+ String entity = response.getEntity();
+
+ Map map = JsonHelper.jsonToMap( entity );
+
+ assertNotNull( map );
+ assertTrue( map.containsKey( "self" ) );
+
+ }
+
+ @Documented( "Delete node." )
+ @Test
+ public void shouldRespondWith204WhenNodeDeleted() throws Exception
+ {
+ long node = helper.createNode();
+ gen.get().description( startGraph( "delete node" ) )
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.dataUri() + "node/" + node );
+ }
+
+ @Test
+ public void shouldRespondWith404AndSensibleEntityBodyWhenNodeToBeDeletedCannotBeFound() throws Exception
+ {
+ JaxRsResponse response = sendDeleteRequestToServer(NON_EXISTENT_NODE_ID);
+ assertEquals( 404, response.getStatus() );
+
+ Map jsonMap = JsonHelper.jsonToMap( response.getEntity() );
+ assertThat( jsonMap, hasKey( "message" ) );
+ assertNotNull( jsonMap.get( "message" ) );
+ }
+
+ @Documented( "Nodes with relationships cannot be deleted.\n" +
+ "\n" +
+ "The relationships on a node has to be deleted before the node can be\n" +
+ "deleted.\n" +
+ " \n" +
+ "TIP: You can use `DETACH DELETE` in Cypher to delete nodes and their relationships in one go." )
+ @Test
+ public void shouldRespondWith409AndSensibleEntityBodyWhenNodeCannotBeDeleted() throws Exception
+ {
+ long id = helper.createNode();
+ helper.createRelationship( "LOVES", id, helper.createNode() );
+ JaxRsResponse response = sendDeleteRequestToServer(id);
+ assertEquals( 409, response.getStatus() );
+ Map jsonMap = JsonHelper.jsonToMap( response.getEntity() );
+ assertThat( jsonMap, hasKey( "message" ) );
+ assertNotNull( jsonMap.get( "message" ) );
+
+ gen.get().description( startGraph( "nodes with rels can not be deleted" ) )
+ .expectedStatus( 409 )
+ .delete( functionalTestHelper.dataUri() + "node/" + id );
+ }
+
+ @Test
+ public void shouldRespondWith400IfInvalidJsonSentAsNodePropertiesDuringNodeCreation() throws URISyntaxException
+ {
+ String mangledJsonArray = "{\"myprop\":[1,2,\"three\"]}";
+ JaxRsResponse response = sendCreateRequestToServer(mangledJsonArray);
+ assertEquals( 400, response.getStatus() );
+ assertEquals( "text/plain", response.getType()
+ .toString() );
+ assertThat( response.getEntity(), containsString( mangledJsonArray ) );
+ }
+
+ @Test
+ public void shouldRespondWith400IfInvalidJsonSentAsNodeProperty() throws URISyntaxException {
+ URI nodeLocation = sendCreateRequestToServer().getLocation();
+
+ String mangledJsonArray = "[1,2,\"three\"]";
+ JaxRsResponse response = RestRequest.req().put(nodeLocation.toString() + "/properties/myprop", mangledJsonArray);
+ assertEquals(400, response.getStatus());
+ assertEquals("text/plain", response.getType()
+ .toString());
+ assertThat( response.getEntity(), containsString(mangledJsonArray));
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith400IfInvalidJsonSentAsNodeProperties() throws URISyntaxException {
+ URI nodeLocation = sendCreateRequestToServer().getLocation();
+
+ String mangledJsonProperties = "{\"a\":\"b\", \"c\":[1,2,\"three\"]}";
+ JaxRsResponse response = RestRequest.req().put(nodeLocation.toString() + "/properties", mangledJsonProperties);
+ assertEquals(400, response.getStatus());
+ assertEquals("text/plain", response.getType()
+ .toString());
+ assertThat( response.getEntity(), containsString(mangledJsonProperties));
+ response.close();
+ }
+
+ private JaxRsResponse sendDeleteRequestToServer(final long id) throws Exception
+ {
+ return RestRequest.req().delete(functionalTestHelper.dataUri() + "node/" + id);
+ }
+
+ /*
+ Note that when running this test from within an IDE, the version field will be an empty string. This is because the
+ code that generates the version identifier is written by Maven as part of the build process(!). The tests will pass
+ both in the IDE (where the empty string will be correctly compared).
+ */
+ public static class CommunityVersionAndEditionServiceDocIT extends ExclusiveServerTestBase
+ {
+ private static NeoServer server;
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @ClassRule
+ public static TemporaryFolder staticFolder = new TemporaryFolder();
+
+ public
+ @Rule
+ TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+ private static FakeClock clock;
+
+ @Before
+ public void setUp()
+ {
+ gen.get().setSection( "dev/rest-api/database-version" );
+ }
+
+ @BeforeClass
+ public static void setupServer() throws Exception
+ {
+ clock = new FakeClock();
+ server = CommunityServerBuilder.server()
+ .usingDataDir( staticFolder.getRoot().getAbsolutePath() )
+ .withClock( clock )
+ .build();
+
+ suppressAll().call( new Callable()
+ {
+ @Override
+ public Void call() throws Exception
+ {
+ server.start();
+ return null;
+ }
+ } );
+ functionalTestHelper = new FunctionalTestHelper( server );
+ }
+
+ @Before
+ public void setupTheDatabase() throws Exception
+ {
+ // do nothing, we don't care about the database contents here
+ }
+
+ @AfterClass
+ public static void stopServer() throws Exception
+ {
+ suppressAll().call( new Callable()
+ {
+ @Override
+ public Void call() throws Exception
+ {
+ server.stop();
+ return null;
+ }
+ } );
+ }
+
+ @Test
+ public void shouldReportCommunityEdition() throws Exception
+ {
+ // Given
+ String releaseVersion = server.getDatabase().getGraph().getDependencyResolver().resolveDependency( KernelData
+ .class ).version().getReleaseVersion();
+
+ // When
+ HTTP.Response res =
+ HTTP.GET( functionalTestHelper.managementUri() + "/" + VersionAndEditionService.SERVER_PATH );
+
+ // Then
+ assertEquals( 200, res.status() );
+ assertThat( res.get( "edition" ).asText(), equalTo( "community" ) );
+ assertThat( res.get( "version" ).asText(), equalTo( releaseVersion ) );
+ }
+ }
+
+ public static class ConfigureEnabledManagementConsolesDocIT extends ExclusiveServerTestBase
+ {
+ private NeoServer server;
+
+ @After
+ public void stopTheServer()
+ {
+ server.stop();
+ }
+
+ @Test
+ public void shouldBeAbleToExplicitlySetConsolesToEnabled() throws Exception
+ {
+ server = CommunityServerBuilder.server().withProperty( ServerSettings.console_module_engines.name(), "" )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+
+ assertThat( exec( "ls", "shell" ).getStatus(), is( 400 ) );
+ }
+
+ @Test
+ public void shellConsoleShouldBeEnabledByDefault() throws Exception
+ {
+ server = CommunityServerBuilder.server().usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() ).build();
+ server.start();
+
+ assertThat( exec( "ls", "shell" ).getStatus(), is( 200 ) );
+ }
+
+ private JaxRsResponse exec( String command, String engine )
+ {
+ return RestRequest.req().post( server.baseUri() + "db/manage/server/console", "{" +
+ "\"engine\":\"" + engine + "\"," +
+ "\"command\":\"" + command + "\\n\"}" );
+ }
+ }
+
+ public static class ConsoleServiceDocTest
+ {
+ private final URI uri = URI.create( "http://peteriscool.com:6666/" );
+
+ @Test
+ public void correctRepresentation() throws URISyntaxException
+ {
+ ConsoleService consoleService = new ConsoleService( new ShellOnlyConsoleSessionFactory(), mock( Database.class ),
+ NullLogProvider.getInstance(), new OutputFormat( new JsonFormat(), uri, null ) );
+
+ Response consoleResponse = consoleService.getServiceDefinition();
+
+ assertEquals( 200, consoleResponse.getStatus() );
+ String response = decode( consoleResponse );
+ MatcherAssert.assertThat( response, containsString( "resources" ) );
+ MatcherAssert.assertThat( response, containsString( uri.toString() ) );
+ }
+
+ @Test
+ public void advertisesAvailableConsoleEngines() throws URISyntaxException
+ {
+ ConsoleService consoleServiceWithJustShellEngine = new ConsoleService( new ShellOnlyConsoleSessionFactory(),
+ mock( Database.class ), NullLogProvider.getInstance(), new OutputFormat( new JsonFormat(), uri, null ) );
+
+ String response = decode( consoleServiceWithJustShellEngine.getServiceDefinition());
+
+ MatcherAssert.assertThat( response, containsString( "\"engines\" : [ \"shell\" ]" ) );
+
+ }
+
+ private String decode( final Response response )
+ {
+ return UTF8.decode( (byte[]) response.getEntity() );
+ }
+
+ private static class ShellOnlyConsoleSessionFactory implements ConsoleSessionFactory
+ {
+ @Override
+ public ScriptSession createSession( String engineName, Database database, LogProvider logProvider )
+ {
+ return null;
+ }
+
+ @Override
+ public Iterable supportedEngines()
+ {
+ return Collections.singletonList( "shell" );
+ }
+ }
+ }
+
+ public static class JmxServiceDocTest
+ {
+ public JmxService jmxService;
+ private final URI uri = URI.create( "http://peteriscool.com:6666/" );
+
+ @Test
+ public void correctRepresentation() throws URISyntaxException
+ {
+ Response resp = jmxService.getServiceDefinition();
+
+ assertEquals( 200, resp.getStatus() );
+
+ String json = UTF8.decode( (byte[]) resp.getEntity() );
+ MatcherAssert.assertThat( json, containsString( "resources" ) );
+ MatcherAssert.assertThat( json, containsString( uri.toString() ) );
+ MatcherAssert.assertThat( json, containsString( "jmx/domain/{domain}/{objectName}" ) );
+ }
+
+ @Test
+ public void shouldListDomainsCorrectly() throws Exception
+ {
+ Response resp = jmxService.listDomains();
+
+ assertEquals( 200, resp.getStatus() );
+ }
+
+ @Test
+ public void testwork() throws Exception
+ {
+ jmxService.queryBeans( "[\"*:*\"]" );
+ }
+
+ @Before
+ public void setUp() throws Exception
+ {
+ this.jmxService = new JmxService( new OutputFormat( new JsonFormat(), uri, null ), null );
+ }
+
+ }
+
+ public static class Neo4jShellConsoleSessionDocTest implements ConsoleSessionFactory
+ {
+ private ConsoleService consoleService;
+ private Database database;
+ private final URI uri = URI.create( "http://peteriscool.com:6666/" );
+
+ @Before
+ public void setUp() throws Exception
+ {
+ this.database = new WrappedDatabase( (GraphDatabaseFacade) new TestGraphDatabaseFactory().
+ newImpermanentDatabaseBuilder().
+ setConfig( ShellSettings.remote_shell_enabled, Settings.TRUE ).
+ newGraphDatabase() );
+ this.consoleService = new ConsoleService(
+ this,
+ database,
+ NullLogProvider.getInstance(),
+ new OutputFormat( new JsonFormat(), uri, null ) );
+ }
+
+ @After
+ public void shutdownDatabase()
+ {
+ this.database.getGraph().shutdown();
+ }
+
+ @Override
+ public ScriptSession createSession( String engineName, Database database, LogProvider logProvider )
+ {
+ return new ShellSession( database.getGraph() );
+ }
+
+ @Test
+ public void doesntMangleNewlines() throws Exception
+ {
+ Response response = consoleService.exec( new JsonFormat(),
+ "{ \"command\" : \"create (n) return n;\", \"engine\":\"shell\" }" );
+
+
+ assertEquals( 200, response.getStatus() );
+ String result = decode( response ).get( 0 );
+
+ String expected = "+-----------+" + lineSeparator()
+ + "| n |" + lineSeparator()
+ + "+-----------+" + lineSeparator()
+ + "| Node[0]{} |" + lineSeparator()
+ + "+-----------+" + lineSeparator()
+ + "1 row";
+
+ MatcherAssert.assertThat( result, containsString( expected ) );
+ }
+
+ private List decode( final Response response ) throws JsonParseException
+ {
+ return (List) JsonHelper.readJson( UTF8.decode( (byte[]) response.getEntity() ) );
+ }
+
+ @Override
+ public Iterable supportedEngines()
+ {
+ return new ArrayList()
+ {{
+ add( "shell" );
+ }};
+ }
+ }
+
+ public static class RootServiceDocTest
+ {
+ @Test
+ public void shouldAdvertiseServicesWhenAsked() throws Exception
+ {
+ UriInfo uriInfo = mock( UriInfo.class );
+ URI uri = new URI( "http://example.org:7474/" );
+ when( uriInfo.getBaseUri() ).thenReturn( uri );
+
+ RootService svc = new RootService( new CommunityNeoServer( new Config( stringMap(
+ httpConnector( "1" ).type.name(), "HTTP",
+ httpConnector( "1" ).enabled.name(), "true"
+ ) ),
+ GraphDatabaseDependencies.newDependencies().userLogProvider( NullLogProvider.getInstance() )
+ .monitors( new Monitors() ),
+ NullLogProvider.getInstance() ) );
+
+ EntityOutputFormat output = new EntityOutputFormat( new JsonFormat(), null, null );
+ Response serviceDefinition = svc.getServiceDefinition( uriInfo, output );
+
+ assertEquals( 200, serviceDefinition.getStatus() );
+ Map result = (Map) output.getResultAsMap().get( "services" );
+
+ assertThat( result.get( "console" )
+ .toString(), containsString( String.format( "%sserver/console", uri.toString() ) ) );
+ assertThat( result.get( "jmx" )
+ .toString(), containsString( String.format( "%sserver/jmx", uri.toString() ) ) );
+ }
+ }
+
+ public static class VersionAndEditionServiceTest
+ {
+ @Test
+ public void shouldReturnReadableStringForServiceName() throws Exception
+ {
+ // given
+ VersionAndEditionService service = new VersionAndEditionService( mock( CommunityNeoServer.class ) );
+
+ // when
+ String serviceName = service.getName();
+ // then
+ assertEquals( "version", serviceName );
+ }
+
+ @Test
+ public void shouldReturnSensiblePathWhereServiceIsHosted() throws Exception
+ {
+ // given
+ VersionAndEditionService service = new VersionAndEditionService( mock( CommunityNeoServer.class ) );
+
+ // when
+ String serverPath = service.getServerPath();
+
+ // then
+ assertEquals( "server/version", serverPath );
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/PathsIT.java b/community/server/src/test/java/org/neo4j/server/rest/PathsIT.java
new file mode 100644
index 0000000000000..6abe738831c24
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/PathsIT.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+import org.neo4j.test.GraphDescription.REL;
+import org.neo4j.test.TestData.Title;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class PathsIT extends AbstractRestFunctionalTestBase
+{
+// Layout
+//
+// (e)----------------
+// | |
+// (d)------------- |
+// | \/
+// (a)-(c)-(b)-(f)-(g)
+// |\ / /
+// | ---- /
+// --------
+//
+ @Test
+ @Graph( value = { "a to c", "a to d", "c to b", "d to e", "b to f", "c to f", "f to g", "d to g", "e to g",
+ "c to g" } )
+ @Title( "Find all shortest paths" )
+ @Documented( "The +shortestPath+ algorithm can find multiple paths between the same nodes, like in this example." )
+ public void shouldBeAbleToFindAllShortestPaths() throws JsonParseException
+ {
+
+ // Get all shortest paths
+ long a = nodeId( data.get(), "a" );
+ long g = nodeId( data.get(), "g" );
+ String response = gen()
+ .expectedStatus( Status.OK.getStatusCode() )
+ .payload( getAllShortestPathPayLoad( g ) )
+ .post( "http://localhost:7474/db/data/node/" + a + "/paths" )
+ .entity();
+ Collection> result = (Collection>) JsonHelper.readJson( response );
+ assertEquals( 2, result.size() );
+ for ( Object representation : result )
+ {
+ Map, ?> path = (Map, ?>) representation;
+
+ assertThatPathStartsWith( path, a );
+ assertThatPathEndsWith( path, g );
+ assertThatPathHasLength( path, 2 );
+ }
+ }
+
+// Layout
+//
+// (e)----------------
+// | |
+// (d)------------- |
+// | \/
+// (a)-(c)-(b)-(f)-(g)
+// |\ / /
+// | ---- /
+// --------
+ @Title( "Find one of the shortest paths" )
+ @Test
+ @Graph( value = { "a to c", "a to d", "c to b", "d to e", "b to f", "c to f", "f to g", "d to g", "e to g",
+ "c to g" } )
+ @Documented( "If no path algorithm is specified, a +shortestPath+ algorithm with a max\n" +
+ "depth of 1 will be chosen. In this example, the +max_depth+ is set to +3+\n" +
+ "in order to find the shortest path between a maximum of 3 linked nodes." )
+ public void shouldBeAbleToFetchSingleShortestPath() throws JsonParseException
+ {
+ long a = nodeId( data.get(), "a" );
+ long g = nodeId( data.get(), "g" );
+ String response = gen()
+ .expectedStatus( Status.OK.getStatusCode() )
+ .payload( getAllShortestPathPayLoad( g ) )
+ .post( "http://localhost:7474/db/data/node/" + a + "/path" )
+ .entity();
+ // Get single shortest path
+
+ Map, ?> path = JsonHelper.jsonToMap( response );
+
+ assertThatPathStartsWith( path, a );
+ assertThatPathEndsWith( path, g );
+ assertThatPathHasLength( path, 2 );
+ }
+
+ private void assertThatPathStartsWith( final Map, ?> path, final long start )
+ {
+ assertTrue( "Path should start with " + start + "\nBut it was " + path, path.get( "start" )
+ .toString()
+ .endsWith( "/node/" + start ) );
+ }
+
+ private void assertThatPathEndsWith( final Map, ?> path, final long start )
+ {
+ assertTrue( "Path should end with " + start + "\nBut it was " + path, path.get( "end" )
+ .toString()
+ .endsWith( "/node/" + start ) );
+ }
+
+ private void assertThatPathHasLength( final Map, ?> path, final int length )
+ {
+ Object actual = path.get( "length" );
+
+ assertEquals( "Expected path to have a length of " + length + "\nBut it was " + actual, length, actual );
+ }
+
+// Layout
+//
+// 1.5------(b)--------0.5
+// / \
+// (a)-0.5-(c)-0.5-(d)-0.5-(e)
+// \ /
+// 0.5-------(f)------1.2
+//
+ @Test
+ @Graph( nodes = { @NODE( name = "a", setNameProperty = true ), @NODE( name = "b", setNameProperty = true ),
+ @NODE( name = "c", setNameProperty = true ), @NODE( name = "d", setNameProperty = true ),
+ @NODE( name = "e", setNameProperty = true ), @NODE( name = "f", setNameProperty = true ) }, relationships = {
+ @REL( start = "a", end = "b", type = "to", properties = { @PROP( key = "cost", value = "1.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "a", end = "c", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "a", end = "f", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "c", end = "d", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "d", end = "e", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "b", end = "e", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "f", end = "e", type = "to", properties = { @PROP( key = "cost", value = "1.2", type = GraphDescription.PropType.DOUBLE ) } ) } )
+ @Title( "Execute a Dijkstra algorithm and get a single path" )
+ @Documented( "This example is running a Dijkstra algorithm over a graph with different\n" +
+ "cost properties on different relationships. Note that the request URI\n" +
+ "ends with +/path+ which means a single path is what we want here." )
+ public void shouldGetCorrectDijkstraPathWithWeights() throws Exception
+ {
+ // Get cheapest paths using Dijkstra
+ long a = nodeId( data.get(), "a" );
+ long e = nodeId( data.get(), "e" );
+ String response = gen().expectedStatus( Status.OK.getStatusCode() )
+ .payload( getAllPathsUsingDijkstraPayLoad( e, false ) )
+ .post( "http://localhost:7474/db/data/node/" + a + "/path" )
+ .entity();
+ //
+ Map, ?> path = JsonHelper.jsonToMap( response );
+ assertThatPathStartsWith( path, a );
+ assertThatPathEndsWith( path, e );
+ assertThatPathHasLength( path, 3 );
+ assertEquals( 1.5, path.get( "weight" ) );
+ }
+
+// Layout
+//
+// 1.5------(b)--------0.5
+// / \
+// (a)-0.5-(c)-0.5-(d)-0.5-(e)
+// \ /
+// 0.5-------(f)------1.0
+//
+ @Test
+ @Graph( nodes = { @NODE( name = "a", setNameProperty = true ), @NODE( name = "b", setNameProperty = true ),
+ @NODE( name = "c", setNameProperty = true ), @NODE( name = "d", setNameProperty = true ),
+ @NODE( name = "e", setNameProperty = true ), @NODE( name = "f", setNameProperty = true ) }, relationships = {
+ @REL( start = "a", end = "b", type = "to", properties = { @PROP( key = "cost", value = "1.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "a", end = "c", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "a", end = "f", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "c", end = "d", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "d", end = "e", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "b", end = "e", type = "to", properties = { @PROP( key = "cost", value = "0.5", type = GraphDescription.PropType.DOUBLE ) } ),
+ @REL( start = "f", end = "e", type = "to", properties = { @PROP( key = "cost", value = "1.0", type = GraphDescription.PropType.DOUBLE ) } ) } )
+ @Title( "Execute a Dijkstra algorithm and get multiple paths" )
+ @Documented( "This example is running a Dijkstra algorithm over a graph with different\n" +
+ "cost properties on different relationships. Note that the request URI\n" +
+ "ends with +/paths+ which means we want multiple paths returned, in case\n" +
+ "they exist." )
+ public void shouldGetCorrectDijkstraPathsWithWeights() throws Exception
+ {
+ // Get cheapest paths using Dijkstra
+ long a = nodeId( data.get(), "a" );
+ long e = nodeId( data.get(), "e" );
+ String response = gen().expectedStatus( Status.OK.getStatusCode() )
+ .payload( getAllPathsUsingDijkstraPayLoad( e, false ) )
+ .post( "http://localhost:7474/db/data/node/" + a + "/paths" )
+ .entity();
+ //
+ List> list = JsonHelper.jsonToList( response );
+ assertEquals( 2, list.size() );
+ Map firstPath = list.get( 0 );
+ Map secondPath = list.get( 1 );
+ System.out.println( firstPath );
+ System.out.println( secondPath );
+ assertThatPathStartsWith( firstPath, a );
+ assertThatPathStartsWith( secondPath, a );
+ assertThatPathEndsWith( firstPath, e );
+ assertThatPathEndsWith( secondPath, e );
+ assertEquals( 1.5, firstPath.get( "weight" ) );
+ assertEquals( 1.5, secondPath.get( "weight" ) );
+ // 5 = 3 + 2
+ assertEquals( 5, (Integer) firstPath.get( "length" ) + (Integer) secondPath.get( "length" ) );
+ assertEquals( 1, Math.abs( (Integer) firstPath.get( "length" ) - (Integer) secondPath.get( "length" ) ) );
+ }
+
+// Layout
+//
+// 1------(b)-----1
+// / \
+// (a)-1-(c)-1-(d)-1-(e)
+// \ /
+// 1------(f)-----1
+//
+ @Test
+ @Graph( nodes = { @NODE( name = "a", setNameProperty = true ),
+ @NODE( name = "b", setNameProperty = true ), @NODE( name = "c", setNameProperty = true ),
+ @NODE( name = "d", setNameProperty = true ), @NODE( name = "e", setNameProperty = true ),
+ @NODE( name = "f", setNameProperty = true ) }, relationships = {
+ @REL( start = "a", end = "b", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "a", end = "c", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "a", end = "f", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "c", end = "d", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "d", end = "e", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "b", end = "e", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ),
+ @REL( start = "f", end = "e", type = "to", properties = { @PROP( key = "cost", value = "1", type = GraphDescription.PropType.INTEGER ) } ) } )
+ @Title( "Execute a Dijkstra algorithm with equal weights on relationships" )
+ @Documented( "The following is executing a Dijkstra search on a graph with equal\n" +
+ "weights on all relationships. This example is included to show the\n" +
+ "difference when the same graph structure is used, but the path weight is\n" +
+ "equal to the number of hops." )
+ public void shouldGetCorrectDijkstraPathsWithEqualWeightsWithDefaultCost() throws Exception
+ {
+ // Get cheapest path using Dijkstra
+ long a = nodeId( data.get(), "a" );
+ long e = nodeId( data.get(), "e" );
+ String response = gen()
+ .expectedStatus( Status.OK.getStatusCode() )
+ .payload( getAllPathsUsingDijkstraPayLoad( e, false ) )
+ .post( "http://localhost:7474/db/data/node/" + a + "/path" )
+ .entity();
+
+ Map, ?> path = JsonHelper.jsonToMap( response );
+ assertThatPathStartsWith( path, a );
+ assertThatPathEndsWith( path, e );
+ assertThatPathHasLength( path, 2 );
+ assertEquals( 2.0, path.get( "weight" ) );
+ }
+
+// Layout
+//
+// (e)----------------
+// | |
+// (d)------------- |
+// | \/
+// (a)-(c)-(b)-(f)-(g)
+// |\ / /
+// | ---- /
+// --------
+ @Test
+ @Graph( value = { "a to c", "a to d", "c to b", "d to e", "b to f", "c to f", "f to g", "d to g", "e to g",
+ "c to g" } )
+ public void shouldReturn404WhenFailingToFindASinglePath() throws JsonParseException
+ {
+ long a = nodeId( data.get(), "a" );
+ long g = nodeId( data.get(), "g" );
+ String noHitsJson = "{\"to\":\""
+ + nodeUri( g )
+ + "\", \"max_depth\":1, \"relationships\":{\"type\":\"dummy\", \"direction\":\"in\"}, \"algorithm\":\"shortestPath\"}";
+ String entity = gen()
+ .expectedStatus( Status.NOT_FOUND.getStatusCode() )
+ .payload( noHitsJson )
+ .post( "http://localhost:7474/db/data/node/" + a + "/path" )
+ .entity();
+ System.out.println( entity );
+ }
+
+ private long nodeId( final Map map, final String string )
+ {
+ return map.get( string )
+ .getId();
+ }
+
+ private String nodeUri( final long l )
+ {
+ return NODES + l;
+ }
+
+ private String getAllShortestPathPayLoad( final long to )
+ {
+ String json = "{\"to\":\""
+ + nodeUri( to )
+ + "\", \"max_depth\":3, \"relationships\":{\"type\":\"to\", \"direction\":\"out\"}, \"algorithm\":\"shortestPath\"}";
+ return json;
+ }
+
+ //
+ private String getAllPathsUsingDijkstraPayLoad( final long to, final boolean includeDefaultCost )
+ {
+ String json = "{\"to\":\"" + nodeUri( to ) + "\"" + ", \"cost_property\":\"cost\""
+ + ( includeDefaultCost ? ", \"default_cost\":1" : "" )
+ + ", \"relationships\":{\"type\":\"to\", \"direction\":\"out\"}, \"algorithm\":\"dijkstra\"}";
+ return json;
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RESTDocsGenerator.java b/community/server/src/test/java/org/neo4j/server/rest/RESTDocsGenerator.java
index dd2886abfafa3..399e05148a3c5 100644
--- a/community/server/src/test/java/org/neo4j/server/rest/RESTDocsGenerator.java
+++ b/community/server/src/test/java/org/neo4j/server/rest/RESTDocsGenerator.java
@@ -19,14 +19,6 @@
*/
package org.neo4j.server.rest;
-import com.sun.jersey.api.client.Client;
-import com.sun.jersey.api.client.ClientRequest;
-import com.sun.jersey.api.client.ClientRequest.Builder;
-import com.sun.jersey.api.client.ClientResponse;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
@@ -39,19 +31,20 @@
import java.util.function.Predicate;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
+
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientRequest;
+import com.sun.jersey.api.client.ClientRequest.Builder;
+import com.sun.jersey.api.client.ClientResponse;
import org.neo4j.doc.tools.AsciiDocGenerator;
import org.neo4j.function.Predicates;
-import org.neo4j.graphdb.Transaction;
import org.neo4j.helpers.collection.Pair;
import org.neo4j.test.GraphDefinition;
import org.neo4j.test.TestData.Producer;
-import org.neo4j.visualization.asciidoc.AsciidocHelper;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
/**
* Generate asciidoc-formatted documentation from HTTP requests and responses.
@@ -65,8 +58,6 @@
*/
public class RESTDocsGenerator extends AsciiDocGenerator
{
- private static final String EQUAL_SIGNS = "======";
-
private static final Builder REQUEST_BUILDER = ClientRequest.create();
private static final List RESPONSE_HEADERS = Arrays.asList( "Content-Type", "Location" );
@@ -96,9 +87,6 @@ public void destroy( RESTDocsGenerator product, boolean successful )
private final List>> expectedHeaderFields = new ArrayList<>();
private String payload;
private final Map addedRequestHeaders = new TreeMap<>( );
- private boolean noDoc = false;
- private boolean noGraph = false;
- private int headingLevel = 3;
/**
* Creates a documented test case. Finish building it by using one of these:
@@ -196,33 +184,6 @@ public RESTDocsGenerator payload( final String payload )
return this;
}
- public RESTDocsGenerator noDoc() {
- this.noDoc = true;
- return this;
- }
-
- public RESTDocsGenerator noGraph()
- {
- this.noGraph = true;
- return this;
- }
-
- /**
- * Set a custom heading level. Defaults to 3.
- *
- * @param headingLevel a value between 1 and 6 (inclusive)
- */
- public RESTDocsGenerator docHeadingLevel( final int headingLevel )
- {
- if ( headingLevel < 1 || headingLevel > EQUAL_SIGNS.length() )
- {
- throw new IllegalArgumentException( "Heading level out of bounds: "
- + headingLevel );
- }
- this.headingLevel = headingLevel;
- return this;
- }
-
/**
* Add an expected response header. If the heading is missing in the
* response the test will fail. The header and its value are also included
@@ -232,7 +193,7 @@ public RESTDocsGenerator docHeadingLevel( final int headingLevel )
*/
public RESTDocsGenerator expectedHeader( final String expectedHeaderField )
{
- this.expectedHeaderFields.add( Pair.of(expectedHeaderField, Predicates.notNull()) );
+ this.expectedHeaderFields.add( Pair.of(expectedHeaderField, Predicates.notNull()) );
return this;
}
@@ -413,10 +374,6 @@ private ResponseEntity retrieveResponse( final String title, final String descri
assertTrue( "wrong headers: " + response.getHeaders(), headerField.other().test( response.getHeaders()
.getFirst( headerField.first() ) ) );
}
- if ( noDoc )
- {
- data.setIgnore();
- }
data.setTitle( title );
data.setDescription( description );
data.setMethod( request.getMethod() );
@@ -424,17 +381,6 @@ private ResponseEntity retrieveResponse( final String title, final String descri
data.setStatus( responseCode );
assertEquals( "Wrong response status. response: " + data.entity, responseCode, response.getStatus() );
getResponseHeaders( data, response.getHeaders(), headerNames(headerFields) );
- if ( graph == null )
- {
- document( data );
- }
- else
- {
- try ( Transaction transaction = graph.beginTx() )
- {
- document( data );
- }
- }
return new ResponseEntity( response, data.entity );
}
@@ -515,137 +461,4 @@ public JaxRsResponse response()
return response;
}
}
-
- protected void document( final DocumentationData data )
- {
- if (data.ignore)
- {
- return;
- }
- String name = data.title.replace( " ", "-" ).toLowerCase();
- String filename = name + ".asciidoc";
- File dir = new File( new File( new File( "target" ), "docs" ), section );
- data.description = replaceSnippets( data.description, dir, name );
- try ( Writer fw = AsciiDocGenerator.getFW( dir, filename ) )
- {
- String longSection = section.replaceAll( "\\(|\\)", "" )+"-" + name.replaceAll( "\\(|\\)", "" );
- if(longSection.indexOf( "/" )>0)
- {
- longSection = longSection.substring( longSection.indexOf( "/" )+1 );
- }
- line( fw, "[[" + longSection + "]]" );
- //make first Character uppercase
- String firstChar = data.title.substring( 0, 1 ).toUpperCase();
- String heading = firstChar + data.title.substring( 1 );
- line( fw, getAsciidocHeading( heading ) );
- line( fw, "" );
- if ( data.description != null && !data.description.isEmpty() )
- {
- line( fw, data.description );
- line( fw, "" );
- }
- if ( !noGraph && graph != null )
- {
- fw.append( AsciiDocGenerator.dumpToSeparateFile( dir,
- name + ".graph",
- AsciidocHelper.createGraphVizWithNodeId( "Final Graph",
- graph, title ) ) );
- line(fw, "" );
- }
- line( fw, "_Example request_" );
- line( fw, "" );
- StringBuilder sb = new StringBuilder( 512 );
- sb.append( "* *+" )
- .append( data.method )
- .append( "+* +" )
- .append( data.uri )
- .append( "+\n" );
- if ( data.requestHeaders != null )
- {
- for ( Entry header : data.requestHeaders.entrySet() )
- {
- sb.append( "* *+" )
- .append( header.getKey() )
- .append( ":+* +" )
- .append( header.getValue() )
- .append( "+\n" );
- }
- }
- String prettifiedPayload = data.getPayload();
- if ( prettifiedPayload != null )
- {
- writeEntity( sb, prettifiedPayload );
- }
- fw.append( AsciiDocGenerator.dumpToSeparateFile( dir, name
- + ".request",
- sb.toString() ) );
- sb = new StringBuilder( 2048 );
- line( fw, "" );
- line( fw, "_Example response_" );
- line( fw, "" );
- int statusCode = data.status;
- sb.append( "* *+" )
- .append( statusCode )
- .append( ":+* +" )
- .append( statusNameFromStatusCode( statusCode ) )
- .append( "+\n" );
- if ( data.responseHeaders != null )
- {
- for ( Entry header : data.responseHeaders.entrySet() )
- {
- sb.append( "* *+" )
- .append( header.getKey() )
- .append( ":+* +" )
- .append( header.getValue() )
- .append( "+\n" );
- }
- }
- writeEntity( sb, data.getPrettifiedEntity() );
- fw.append( AsciiDocGenerator.dumpToSeparateFile( dir,
- name + ".response", sb.toString() ) );
- line( fw, "" );
- }
- catch ( IOException e )
- {
- e.printStackTrace();
- fail();
- }
- }
-
- private String statusNameFromStatusCode( int statusCode )
- {
- Object name = Response.Status.fromStatusCode( statusCode );
- if ( name == null )
- {
- switch ( statusCode )
- {
- case 405:
- name = "Method Not Allowed";
- break;
- case 422:
- name = "Unprocessable Entity";
- break;
- default:
- throw new RuntimeException( "Missing name for status code: [" + statusCode + "]." );
- }
- }
- return String.valueOf( name );
- }
-
- private String getAsciidocHeading( final String heading )
- {
- String equalSigns = EQUAL_SIGNS.substring( 0, headingLevel );
- return equalSigns + ' ' + heading + ' ' + equalSigns;
- }
-
- public void writeEntity( final StringBuilder sb, final String entity )
- {
- if ( entity != null )
- {
- sb.append( "\n[source,javascript]\n" )
- .append( "----\n" )
- .append( entity )
- .append( "\n----\n\n" );
- }
- }
}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RedirectorIT.java b/community/server/src/test/java/org/neo4j/server/rest/RedirectorIT.java
new file mode 100644
index 0000000000000..6100d60df59cf
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RedirectorIT.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class RedirectorIT extends AbstractRestFunctionalTestBase
+{
+ @Test
+ public void shouldRedirectRootToBrowser() throws Exception {
+ JaxRsResponse response = new RestRequest(server().baseUri()).get();
+
+ assertThat(response.getStatus(), is(not(404)));
+ }
+
+ @Test
+ public void shouldNotRedirectTheRestOfTheWorld() throws Exception {
+ JaxRsResponse response = new RestRequest(server().baseUri()).get("a/different/relative/data/uri/");
+
+ assertThat(response.getStatus(), is(404));
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RelationshipIT.java b/community/server/src/test/java/org/neo4j/server/rest/RelationshipIT.java
new file mode 100644
index 0000000000000..62dbd225f70f0
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RelationshipIT.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import javax.ws.rs.core.Response.Status;
+
+import com.sun.jersey.api.client.ClientResponse;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.graphdb.Direction;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Relationship;
+import org.neo4j.graphdb.RelationshipType;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.repr.StreamingFormat;
+import org.neo4j.test.GraphDescription;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+import org.neo4j.test.GraphDescription.REL;
+import org.neo4j.test.TestData.Title;
+
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.graphdb.Neo4jMatchers.hasProperty;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+
+public class RelationshipIT extends AbstractRestFunctionalDocTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Test
+ @Title("Remove properties from a relationship")
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void shouldReturn204WhenPropertiesAreRemovedFromRelationship()
+ {
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ gen().description( startGraph( "remove properties from a relationship" ) )
+ .expectedStatus( Status.NO_CONTENT.getStatusCode() )
+ .delete( functionalTestHelper.relationshipPropertiesUri( loves.getId() ) ).entity();
+ }
+
+ @Test
+ @Graph("I know you")
+ public void get_Relationship_by_ID() throws JsonParseException
+ {
+ Node node = data.get().get( "I" );
+ Relationship relationship;
+ try ( Transaction transaction = node.getGraphDatabase().beginTx() )
+ {
+ relationship = node.getSingleRelationship(
+ RelationshipType.withName( "know" ),
+ Direction.OUTGOING );
+ }
+ String response = gen().expectedStatus(
+ com.sun.jersey.api.client.ClientResponse.Status.OK.getStatusCode() ).get(
+ getRelationshipUri( relationship ) ).entity();
+ assertTrue( JsonHelper.jsonToMap( response ).containsKey( "start" ) );
+ }
+
+ @Test
+ @Title("Remove property from a relationship")
+ @Documented( "See the example request below." )
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void shouldReturn204WhenPropertyIsRemovedFromRelationship()
+ {
+ data.get();
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ gen().description(
+ startGraph( "Remove property from a relationship1" ) );
+ gen().expectedStatus( Status.NO_CONTENT.getStatusCode() ).delete(
+ getPropertiesUri( loves ) + "/cost" ).entity();
+
+ }
+
+ @Test
+ @Title("Remove non-existent property from a relationship")
+ @Documented( "Attempting to remove a property that doesn't exist results in an error." )
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void shouldReturn404WhenPropertyWhichDoesNotExistRemovedFromRelationship()
+ {
+ data.get();
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ gen().description( startGraph( "remove non-existent property from relationship" ) )
+ .expectedStatus( Status.NOT_FOUND.getStatusCode() )
+ .delete( getPropertiesUri( loves ) + "/non-existent" ).entity();
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void shouldReturn404WhenPropertyWhichDoesNotExistRemovedFromRelationshipStreaming()
+ {
+ data.get();
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ gen().withHeader( StreamingFormat.STREAM_HEADER, "true" ).expectedStatus( Status.NOT_FOUND.getStatusCode
+ () ).delete(
+ getPropertiesUri( loves ) + "/non-existent" ).entity();
+ }
+
+ @Test
+ @Graph( "I know you" )
+ @Title( "Remove properties from a non-existing relationship" )
+ @Documented( "Attempting to remove all properties from a relationship which doesn't exist results in an error." )
+ public void shouldReturn404WhenPropertiesRemovedFromARelationshipWhichDoesNotExist()
+ {
+ data.get();
+ gen().expectedStatus( Status.NOT_FOUND.getStatusCode() )
+ .delete( functionalTestHelper.relationshipPropertiesUri( 1234L ) )
+ .entity();
+ }
+
+ @Test
+ @Graph( "I know you" )
+ @Title( "Remove property from a non-existing relationship" )
+ @Documented( "Attempting to remove a property from a relationship which doesn't exist results in an error." )
+ public void shouldReturn404WhenPropertyRemovedFromARelationshipWhichDoesNotExist()
+ {
+ data.get();
+ gen().expectedStatus( Status.NOT_FOUND.getStatusCode() )
+ .delete(
+ functionalTestHelper.relationshipPropertiesUri( 1234L )
+ + "/cost" )
+ .entity();
+
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ @Title("Delete relationship")
+ public void removeRelationship()
+ {
+ data.get();
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ gen().description( startGraph( "Delete relationship1" ) );
+ gen().expectedStatus( Status.NO_CONTENT.getStatusCode() ).delete(
+ getRelationshipUri( loves ) ).entity();
+
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void get_single_property_on_a_relationship() throws Exception
+ {
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ String response = gen().expectedStatus( ClientResponse.Status.OK ).get( getRelPropURI( loves,
+ "cost" ) ).entity();
+ assertTrue( response.contains( "high" ) );
+ }
+
+ private String getRelPropURI( Relationship loves, String propertyKey )
+ {
+ return getRelationshipUri( loves ) + "/properties/" + propertyKey;
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING)})})
+ public void set_single_property_on_a_relationship() throws Exception
+ {
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ assertThat( loves, inTx( graphdb(), hasProperty( "cost" ).withValue( "high" ) ) );
+ gen().description( startGraph( "Set relationship property1" ) );
+ gen().expectedStatus( ClientResponse.Status.NO_CONTENT ).payload( "\"deadly\"" ).put( getRelPropURI( loves,
+ "cost" ) ).entity();
+ assertThat( loves, inTx( graphdb(), hasProperty( "cost" ).withValue( "deadly" ) ) );
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING), @PROP(key = "since", value = "1day", type = GraphDescription.PropType.STRING)})})
+ public void set_all_properties_on_a_relationship() throws Exception
+ {
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ assertThat( loves, inTx( graphdb(), hasProperty( "cost" ).withValue( "high" ) ) );
+ gen().description( startGraph( "Set relationship property1" ) );
+ gen().expectedStatus( ClientResponse.Status.NO_CONTENT ).payload( JsonHelper.createJsonFrom( MapUtil.map(
+ "happy", false ) ) ).put( getRelPropsURI( loves ) ).entity();
+ assertThat( loves, inTx( graphdb(), hasProperty( "happy" ).withValue( false ) ) );
+ assertThat( loves, inTx( graphdb(), not( hasProperty( "cost" ) ) ) );
+ }
+
+ @Test
+ @Graph(nodes = {@NODE(name = "Romeo", setNameProperty = true),
+ @NODE(name = "Juliet", setNameProperty = true)}, relationships = {@REL(start = "Romeo", end = "Juliet",
+ type = "LOVES", properties = {@PROP(key = "cost", value = "high", type = GraphDescription.PropType
+ .STRING), @PROP(key = "since", value = "1day", type = GraphDescription.PropType.STRING)})})
+ public void get_all_properties_on_a_relationship() throws Exception
+ {
+ Relationship loves = getFirstRelationshipFromRomeoNode();
+ String response = gen().expectedStatus( ClientResponse.Status.OK ).get( getRelPropsURI( loves ) ).entity();
+ assertTrue( response.contains( "high" ) );
+ }
+
+ private Relationship getFirstRelationshipFromRomeoNode()
+ {
+ Node romeo = getNode( "Romeo" );
+
+ try ( Transaction transaction = romeo.getGraphDatabase().beginTx() )
+ {
+ return romeo.getRelationships().iterator().next();
+ }
+ }
+
+ private String getRelPropsURI( Relationship rel )
+ {
+ return getRelationshipUri( rel ) + "/properties";
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RemoveNodePropertiesIT.java b/community/server/src/test/java/org/neo4j/server/rest/RemoveNodePropertiesIT.java
new file mode 100644
index 0000000000000..d52e9d335851b
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RemoveNodePropertiesIT.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+
+import static org.junit.Assert.assertEquals;
+
+public class RemoveNodePropertiesIT extends AbstractRestFunctionalDocTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ private String getPropertiesUri( final long nodeId )
+ {
+ return functionalTestHelper.nodePropertiesUri( nodeId );
+ }
+
+ @Test
+ public void shouldReturn204WhenPropertiesAreRemoved()
+ {
+ long nodeId = helper.createNode();
+ Map map = new HashMap();
+ map.put( "jim", "tobias" );
+ helper.setNodeProperties( nodeId, map );
+ JaxRsResponse response = removeNodePropertiesOnServer(nodeId);
+ assertEquals( 204, response.getStatus() );
+ response.close();
+ }
+
+ @Documented( "Delete all properties from node." )
+ @Test
+ public void shouldReturn204WhenAllPropertiesAreRemoved()
+ {
+ long nodeId = helper.createNode();
+ Map map = new HashMap();
+ map.put( "jim", "tobias" );
+ helper.setNodeProperties( nodeId, map );
+ gen.get().description( startGraph( "delete all prps from node" ) )
+ .expectedStatus( 204 )
+ .delete( functionalTestHelper.nodePropertiesUri( nodeId ) );
+ }
+
+ @Test
+ public void shouldReturn404WhenPropertiesSentToANodeWhichDoesNotExist() {
+ JaxRsResponse response = RestRequest.req().delete(getPropertiesUri(999999));
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ private JaxRsResponse removeNodePropertiesOnServer(final long nodeId)
+ {
+ return RestRequest.req().delete(getPropertiesUri(nodeId));
+ }
+
+ @Documented( "To delete a single property\n" +
+ "from a node, see the example below" )
+ @Test
+ public void delete_a_named_property_from_a_node()
+ {
+ long nodeId = helper.createNode();
+ Map map = new HashMap();
+ map.put( "name", "tobias" );
+ helper.setNodeProperties( nodeId, map );
+ gen.get()
+ .expectedStatus( 204 )
+ .description( startGraph( "delete named property start" ))
+ .delete( functionalTestHelper.nodePropertyUri( nodeId, "name") );
+ }
+
+ @Test
+ public void shouldReturn404WhenRemovingNonExistingNodeProperty()
+ {
+ long nodeId = helper.createNode();
+ Map map = new HashMap();
+ map.put( "jim", "tobias" );
+ helper.setNodeProperties( nodeId, map );
+ JaxRsResponse response = removeNodePropertyOnServer(nodeId, "foo");
+ assertEquals(404, response.getStatus());
+ }
+
+ @Test
+ public void shouldReturn404WhenPropertySentToANodeWhichDoesNotExist() {
+ JaxRsResponse response = RestRequest.req().delete(getPropertyUri(999999, "foo"));
+ assertEquals(404, response.getStatus());
+ }
+
+ private String getPropertyUri( final long nodeId, final String key )
+ {
+ return functionalTestHelper.nodePropertyUri( nodeId, key );
+ }
+
+ private JaxRsResponse removeNodePropertyOnServer(final long nodeId, final String key)
+ {
+ return RestRequest.req().delete(getPropertyUri(nodeId, key));
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RemoveRelationshipIT.java b/community/server/src/test/java/org/neo4j/server/rest/RemoveRelationshipIT.java
new file mode 100644
index 0000000000000..7a5832138b046
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RemoveRelationshipIT.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+
+import static org.junit.Assert.assertEquals;
+
+public class RemoveRelationshipIT extends AbstractRestFunctionalTestBase
+{
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Test
+ public void shouldGet204WhenRemovingAValidRelationship() throws Exception
+ {
+ long relationshipId = helper.createRelationship( "KNOWS" );
+
+ JaxRsResponse response = sendDeleteRequest(new URI(functionalTestHelper.relationshipUri(relationshipId)));
+
+ assertEquals( 204, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet404WhenRemovingAnInvalidRelationship() throws Exception
+ {
+ long relationshipId = helper.createRelationship( "KNOWS" );
+
+ JaxRsResponse response = sendDeleteRequest(new URI(
+ functionalTestHelper.relationshipUri((relationshipId + 1) * 9999)));
+
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ private JaxRsResponse sendDeleteRequest(URI requestUri)
+ {
+ return RestRequest.req().delete(requestUri);
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RetrieveNodeIT.java b/community/server/src/test/java/org/neo4j/server/rest/RetrieveNodeIT.java
new file mode 100644
index 0000000000000..8cee5873897fd
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RetrieveNodeIT.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.RESTDocsGenerator.ResponseEntity;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.repr.formats.CompactJsonFormat;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class RetrieveNodeIT extends AbstractRestFunctionalDocTestBase
+{
+ private URI nodeUri;
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Before
+ public void cleanTheDatabaseAndInitialiseTheNodeUri() throws Exception
+ {
+ nodeUri = new URI( functionalTestHelper.nodeUri() + "/"
+ + new GraphDbHelper( server().getDatabase() ).createNode() );
+ }
+
+ @Test
+ public void shouldParameteriseUrisInNodeRepresentationWithHostHeaderValue() throws Exception
+ {
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( nodeUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "Host", "dummy.neo4j.org" );
+ HttpResponse response = httpclient.execute( httpget );
+ HttpEntity entity = response.getEntity();
+
+ String entityBody = IOUtils.toString( entity.getContent(), StandardCharsets.UTF_8 );
+
+ assertThat( entityBody, containsString( "http://dummy.neo4j.org/db/data/node/" ) );
+
+ } finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Test
+ public void shouldParameteriseUrisInNodeRepresentationWithoutHostHeaderUsingRequestUri() throws Exception
+ {
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( nodeUri );
+
+ httpget.setHeader( "Accept", "application/json" );
+ HttpResponse response = httpclient.execute( httpget );
+ HttpEntity entity = response.getEntity();
+
+ String entityBody = IOUtils.toString( entity.getContent(), StandardCharsets.UTF_8 );
+
+ assertThat( entityBody, containsString( nodeUri.toString() ) );
+ } finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Documented( "Get node.\n" +
+ "\n" +
+ "Note that the response contains URI/templates for the available\n" +
+ "operations for getting properties and relationships." )
+ @Test
+ public void shouldGet200WhenRetrievingNode() throws Exception
+ {
+ String uri = nodeUri.toString();
+ gen.get()
+ .expectedStatus( 200 )
+ .get( uri );
+ }
+
+ @Documented( "Get node -- compact.\n" +
+ "\n" +
+ "Specifying the subformat in the requests media type yields a more compact\n" +
+ "JSON response without metadata and templates." )
+ @Test
+ public void shouldGet200WhenRetrievingNodeCompact()
+ {
+ String uri = nodeUri.toString();
+ ResponseEntity entity = gen.get()
+ .expectedType( CompactJsonFormat.MEDIA_TYPE )
+ .expectedStatus( 200 )
+ .get( uri );
+ assertTrue( entity.entity()
+ .contains( "self" ) );
+ }
+
+ @Test
+ public void shouldGetContentLengthHeaderWhenRetrievingNode() throws Exception
+ {
+ JaxRsResponse response = retrieveNodeFromService( nodeUri.toString() );
+ assertNotNull( response.getHeaders()
+ .get( "Content-Length" ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldHaveJsonMediaTypeOnResponse()
+ {
+ JaxRsResponse response = retrieveNodeFromService( nodeUri.toString() );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ response.close();
+ }
+
+ @Test
+ public void shouldHaveJsonDataInResponse() throws Exception
+ {
+ JaxRsResponse response = retrieveNodeFromService( nodeUri.toString() );
+
+ Map map = JsonHelper.jsonToMap( response.getEntity() );
+ assertTrue( map.containsKey( "self" ) );
+ response.close();
+ }
+
+ @Documented( "Get non-existent node." )
+ @Test
+ public void shouldGet404WhenRetrievingNonExistentNode() throws Exception
+ {
+ gen.get()
+ .expectedStatus( 404 )
+ .get( nodeUri + "00000" );
+ }
+
+ private JaxRsResponse retrieveNodeFromService( final String uri )
+ {
+ return RestRequest.req().get( uri );
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/RetrieveRelationshipsFromNodeIT.java b/community/server/src/test/java/org/neo4j/server/rest/RetrieveRelationshipsFromNodeIT.java
new file mode 100644
index 0000000000000..7bffaeb148e94
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/RetrieveRelationshipsFromNodeIT.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.repr.RelationshipRepresentationTest;
+import org.neo4j.server.rest.repr.formats.StreamingJsonFormat;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+public class RetrieveRelationshipsFromNodeIT extends AbstractRestFunctionalDocTestBase
+{
+ private long nodeWithRelationships;
+ private long nodeWithoutRelationships;
+ private long nonExistingNode;
+
+ private static FunctionalTestHelper functionalTestHelper;
+ private static GraphDbHelper helper;
+ private long likes;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ helper = functionalTestHelper.getGraphDbHelper();
+ }
+
+ @Before
+ public void setupTheDatabase()
+ {
+ nodeWithRelationships = helper.createNode();
+ likes = helper.createRelationship( "LIKES", nodeWithRelationships, helper.createNode() );
+ helper.createRelationship( "LIKES", helper.createNode(), nodeWithRelationships );
+ helper.createRelationship( "HATES", nodeWithRelationships, helper.createNode() );
+ nodeWithoutRelationships = helper.createNode();
+ nonExistingNode = nodeWithoutRelationships * 100;
+ }
+
+ private JaxRsResponse sendRetrieveRequestToServer( long nodeId, String path )
+ {
+ return RestRequest.req().get( functionalTestHelper.nodeUri() + "/" + nodeId + "/relationships" + path );
+ }
+
+ private void verifyRelReps( int expectedSize, String json ) throws JsonParseException
+ {
+ List> relreps = JsonHelper.jsonToList( json );
+ assertEquals( expectedSize, relreps.size() );
+ for ( Map relrep : relreps )
+ {
+ RelationshipRepresentationTest.verifySerialisation( relrep );
+ }
+ }
+
+ @Test
+ public void shouldParameteriseUrisInRelationshipRepresentationWithHostHeaderValue() throws Exception
+ {
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( "http://localhost:7474/db/data/relationship/" + likes );
+ httpget.setHeader( "Accept", "application/json" );
+ httpget.setHeader( "Host", "dummy.neo4j.org" );
+ HttpResponse response = httpclient.execute( httpget );
+ HttpEntity entity = response.getEntity();
+
+ String entityBody = IOUtils.toString( entity.getContent(), StandardCharsets.UTF_8 );
+
+ System.out.println( entityBody );
+
+ assertThat( entityBody, containsString( "http://dummy.neo4j.org/db/data/relationship/" + likes ) );
+ assertThat( entityBody, not( containsString( "localhost:7474" ) ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Test
+ public void shouldParameteriseUrisInRelationshipRepresentationWithoutHostHeaderUsingRequestUri() throws Exception
+ {
+ HttpClient httpclient = new DefaultHttpClient();
+ try
+ {
+ HttpGet httpget = new HttpGet( "http://localhost:7474/db/data/relationship/" + likes );
+
+ httpget.setHeader( "Accept", "application/json" );
+ HttpResponse response = httpclient.execute( httpget );
+ HttpEntity entity = response.getEntity();
+
+ String entityBody = IOUtils.toString( entity.getContent(), StandardCharsets.UTF_8 );
+
+ assertThat( entityBody, containsString( "http://localhost:7474/db/data/relationship/" + likes ) );
+ }
+ finally
+ {
+ httpclient.getConnectionManager().shutdown();
+ }
+ }
+
+ @Documented( "Get all relationships." )
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingAllRelationshipsForANode()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithRelationships + "/relationships" + "/all" )
+ .entity();
+ verifyRelReps( 3, entity );
+ }
+
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingAllRelationshipsForANodeStreaming()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .withHeader(StreamingJsonFormat.STREAM_HEADER,"true")
+ .expectedStatus(200)
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithRelationships + "/relationships" + "/all" )
+ .entity();
+ verifyRelReps( 3, entity );
+ }
+
+ @Documented( "Get incoming relationships." )
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingIncomingRelationshipsForANode()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithRelationships + "/relationships" + "/in" )
+ .entity();
+ verifyRelReps( 1, entity );
+ }
+
+ @Documented( "Get outgoing relationships." )
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingOutgoingRelationshipsForANode()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithRelationships + "/relationships" + "/out" )
+ .entity();
+ verifyRelReps( 2, entity );
+ }
+
+ @Documented( "Get typed relationships.\n" +
+ "\n" +
+ "Note that the \"+&+\" needs to be encoded like \"+%26+\" for example when\n" +
+ "using http://curl.haxx.se/[cURL] from the terminal." )
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingAllTypedRelationshipsForANode()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithRelationships + "/relationships"
+ + "/all/LIKES&HATES" )
+ .entity();
+ verifyRelReps( 3, entity );
+ }
+
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingIncomingTypedRelationshipsForANode()
+ throws JsonParseException
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nodeWithRelationships, "/in/LIKES" );
+ assertEquals( 200, response.getStatus() );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ verifyRelReps( 1, response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith200AndListOfRelationshipRepresentationsWhenGettingOutgoingTypedRelationshipsForANode()
+ throws JsonParseException
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nodeWithRelationships, "/out/HATES" );
+ assertEquals( 200, response.getStatus() );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ verifyRelReps( 1, response.getEntity() );
+ response.close();
+ }
+
+ @Documented( "Get relationships on a node without relationships." )
+ @Test
+ public void shouldRespondWith200AndEmptyListOfRelationshipRepresentationsWhenGettingAllRelationshipsForANodeWithoutRelationships()
+ throws JsonParseException
+ {
+ String entity = gen.get()
+ .expectedStatus( 200 )
+ .get( functionalTestHelper.nodeUri() + "/" + nodeWithoutRelationships + "/relationships" + "/all" )
+ .entity();
+ verifyRelReps( 0, entity );
+ }
+
+ @Test
+ public void shouldRespondWith200AndEmptyListOfRelationshipRepresentationsWhenGettingIncomingRelationshipsForANodeWithoutRelationships()
+ throws JsonParseException
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nodeWithoutRelationships, "/in" );
+ assertEquals( 200, response.getStatus() );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ verifyRelReps( 0, response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith200AndEmptyListOfRelationshipRepresentationsWhenGettingOutgoingRelationshipsForANodeWithoutRelationships()
+ throws JsonParseException
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nodeWithoutRelationships, "/out" );
+ assertEquals( 200, response.getStatus() );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ verifyRelReps( 0, response.getEntity() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith404WhenGettingAllRelationshipsForNonExistingNode()
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nonExistingNode, "/all" );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith404WhenGettingIncomingRelationshipsForNonExistingNode()
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nonExistingNode, "/in" );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith404WhenGettingIncomingRelationshipsForNonExistingNodeStreaming()
+ {
+ JaxRsResponse response = RestRequest.req().header(StreamingJsonFormat.STREAM_HEADER,"true").get(functionalTestHelper.nodeUri() + "/" + nonExistingNode + "/relationships" + "/in");
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldRespondWith404WhenGettingOutgoingRelationshipsForNonExistingNode()
+ {
+ JaxRsResponse response = sendRetrieveRequestToServer( nonExistingNode, "/out" );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGet200WhenRetrievingValidRelationship()
+ {
+ long relationshipId = helper.createRelationship( "LIKES" );
+
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.relationshipUri( relationshipId ) );
+
+ assertEquals( 200, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldGetARelationshipRepresentationInJsonWhenRetrievingValidRelationship() throws Exception
+ {
+ long relationshipId = helper.createRelationship( "LIKES" );
+
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.relationshipUri( relationshipId ) );
+
+ String entity = response.getEntity();
+ assertNotNull( entity );
+ isLegalJson( entity );
+ response.close();
+ }
+
+ private void isLegalJson( String entity ) throws IOException, JsonParseException
+ {
+ JsonHelper.jsonToMap( entity );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/SchemaConstraintsIT.java b/community/server/src/test/java/org/neo4j/server/rest/SchemaConstraintsIT.java
new file mode 100644
index 0000000000000..d202472b8607f
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/SchemaConstraintsIT.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.neo4j.function.Factory;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.graphdb.schema.ConstraintDefinition;
+import org.neo4j.graphdb.schema.ConstraintType;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasItems;
+import static org.junit.Assert.assertThat;
+
+import static org.neo4j.graphdb.Label.label;
+import static org.neo4j.graphdb.Neo4jMatchers.containsOnly;
+import static org.neo4j.graphdb.Neo4jMatchers.getConstraints;
+import static org.neo4j.graphdb.Neo4jMatchers.isEmpty;
+import static org.neo4j.helpers.collection.MapUtil.map;
+import static org.neo4j.server.rest.domain.JsonHelper.createJsonFrom;
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToList;
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToMap;
+
+public class SchemaConstraintsIT extends AbstractRestFunctionalTestBase
+{
+ @Documented( "Create uniqueness constraint.\n" +
+ "Create a uniqueness constraint on a property." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void createPropertyUniquenessConstraint() throws JsonParseException
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ Map definition = map( "property_keys", singletonList( propertyKey ) );
+
+ String result = gen.get().expectedStatus( 200 ).payload( createJsonFrom( definition ) ).post(
+ getSchemaConstraintLabelUniquenessUri( labelName ) ).entity();
+
+ Map serialized = jsonToMap( result );
+
+ Map constraint = new HashMap<>( );
+ constraint.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint.put( "label", labelName );
+ constraint.put( "property_keys", singletonList( propertyKey ) );
+
+ assertThat( serialized, equalTo( constraint ) );
+ }
+
+ @Documented( "Get a specific uniqueness constraint.\n" +
+ "Get a specific uniqueness constraint for a label and a property." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void getLabelUniquenessPropertyConstraint() throws JsonParseException
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ createLabelUniquenessPropertyConstraint( labelName, propertyKey );
+
+ String result = gen.get().expectedStatus( 200 ).get(
+ getSchemaConstraintLabelUniquenessPropertyUri( labelName, propertyKey ) ).entity();
+
+ List> serializedList = jsonToList( result );
+
+ Map constraint = new HashMap<>( );
+ constraint.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint.put( "label", labelName );
+ constraint.put( "property_keys", singletonList( propertyKey ) );
+
+ assertThat( serializedList, hasItem( constraint ) );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Documented( "Get all uniqueness constraints for a label." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void getLabelUniquenessPropertyConstraints() throws JsonParseException
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey1 = properties.newInstance(), propertyKey2 = properties.newInstance();
+ createLabelUniquenessPropertyConstraint( labelName, propertyKey1 );
+ createLabelUniquenessPropertyConstraint( labelName, propertyKey2 );
+
+ String result = gen.get().expectedStatus( 200 ).get( getSchemaConstraintLabelUniquenessUri( labelName ) ).entity();
+
+ List> serializedList = jsonToList( result );
+
+ Map constraint1 = new HashMap<>( );
+ constraint1.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint1.put( "label", labelName );
+ constraint1.put( "property_keys", singletonList( propertyKey1 ) );
+
+ Map constraint2 = new HashMap<>( );
+ constraint2.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint2.put( "label", labelName );
+ constraint2.put( "property_keys", singletonList( propertyKey2 ) );
+
+ assertThat( serializedList, hasItems( constraint1, constraint2 ) );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Documented( "Get all constraints for a label." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void getLabelPropertyConstraints() throws JsonParseException
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey1 = properties.newInstance();
+ createLabelUniquenessPropertyConstraint( labelName, propertyKey1 );
+
+ String result = gen.get().expectedStatus( 200 ).get( getSchemaConstraintLabelUri( labelName ) ).entity();
+
+ List> serializedList = jsonToList( result );
+
+ Map constraint1 = new HashMap<>( );
+ constraint1.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint1.put( "label", labelName );
+ constraint1.put( "property_keys", singletonList( propertyKey1 ) );
+
+ assertThat( serializedList, hasItems( constraint1 ) );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Documented( "Get all constraints." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void get_constraints() throws JsonParseException
+ {
+ data.get();
+
+ String labelName1 = labels.newInstance(), propertyKey1 = properties.newInstance();
+ createLabelUniquenessPropertyConstraint( labelName1, propertyKey1 );
+
+ String result = gen.get().expectedStatus( 200 ).get( getSchemaConstraintUri() ).entity();
+
+ List> serializedList = jsonToList( result );
+
+ Map constraint1 = new HashMap<>();
+ constraint1.put( "type", ConstraintType.UNIQUENESS.name() );
+ constraint1.put( "label", labelName1 );
+ constraint1.put( "property_keys", singletonList( propertyKey1 ) );
+
+ assertThat( serializedList, hasItems( constraint1 ) );
+ }
+
+ @Documented( "Drop uniqueness constraint.\n" +
+ "Drop uniqueness constraint for a label and a property." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void drop_constraint() throws Exception
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ ConstraintDefinition constraintDefinition = createLabelUniquenessPropertyConstraint( labelName, propertyKey );
+ assertThat( getConstraints( graphdb(), label( labelName ) ), containsOnly( constraintDefinition ) );
+
+ gen.get().expectedStatus( 204 ).delete( getSchemaConstraintLabelUniquenessPropertyUri( labelName, propertyKey ) ).entity();
+
+ assertThat( getConstraints( graphdb(), label( labelName ) ), isEmpty() );
+ }
+
+ /**
+ * Create an index for a label and property key which already exists.
+ */
+ @Test
+ public void create_existing_constraint()
+ {
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ createLabelUniquenessPropertyConstraint( labelName, propertyKey );
+
+ Map definition = map( "property_keys", singletonList( propertyKey ) );
+ gen.get().expectedStatus( 409 ).payload( createJsonFrom( definition ) )
+ .post( getSchemaConstraintLabelUniquenessUri( labelName ) ).entity();
+ }
+
+ @Test
+ public void drop_non_existent_constraint() throws Exception
+ {
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+
+ gen.get().expectedStatus( 404 )
+ .delete( getSchemaConstraintLabelUniquenessPropertyUri( labelName, propertyKey ) );
+ }
+
+ /**
+ * Creating a compound index should not yet be supported.
+ */
+ @Test
+ public void create_compound_schema_index()
+ {
+ Map definition = map( "property_keys",
+ asList( properties.newInstance(), properties.newInstance() ) );
+
+ gen.get().expectedStatus( 400 )
+ .payload( createJsonFrom( definition ) ).post( getSchemaIndexLabelUri( labels.newInstance() ) );
+ }
+
+ private ConstraintDefinition createLabelUniquenessPropertyConstraint( String labelName, String propertyKey )
+ {
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ ConstraintDefinition constraintDefinition = graphdb().schema().constraintFor( label( labelName ) )
+ .assertPropertyIsUnique( propertyKey ).create();
+ tx.success();
+ return constraintDefinition;
+ }
+ }
+
+ private final Factory labels = UniqueStrings.withPrefix( "label" );
+ private final Factory properties = UniqueStrings.withPrefix( "property" );
+ private final Factory relationshipTypes = UniqueStrings.withPrefix( "relationshipType" );
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/SchemaIndexIT.java b/community/server/src/test/java/org/neo4j/server/rest/SchemaIndexIT.java
new file mode 100644
index 0000000000000..c090a8a318af6
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/SchemaIndexIT.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.neo4j.function.Factory;
+import org.neo4j.graphdb.Neo4jMatchers;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.graphdb.schema.IndexDefinition;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import static org.neo4j.graphdb.Label.label;
+import static org.neo4j.graphdb.Neo4jMatchers.containsOnly;
+import static org.neo4j.helpers.collection.MapUtil.map;
+import static org.neo4j.server.rest.domain.JsonHelper.createJsonFrom;
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToList;
+import static org.neo4j.server.rest.domain.JsonHelper.jsonToMap;
+
+public class SchemaIndexIT extends AbstractRestFunctionalTestBase
+{
+ @Documented( "Create index.\n" +
+ "\n" +
+ "This will start a background job in the database that will create and populate the index.\n" +
+ "You can check the status of your index by listing all the indexes for the relevant label." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void create_index() throws JsonParseException
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ Map definition = map( "property_keys", singletonList( propertyKey ) );
+
+ String result = gen.get()
+ .expectedStatus( 200 )
+ .payload( createJsonFrom( definition ) )
+ .post( getSchemaIndexLabelUri( labelName ) )
+ .entity();
+
+ Map serialized = jsonToMap( result );
+
+
+ Map index = new HashMap<>();
+ index.put( "label", labelName );
+ index.put( "property_keys", singletonList( propertyKey ) );
+
+ assertThat( serialized, equalTo( index ) );
+ }
+
+ @Documented( "List indexes for a label." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void get_indexes_for_label() throws Exception
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ createIndex( labelName, propertyKey );
+ Map definition = map( "property_keys", singletonList( propertyKey ) );
+
+ List> serializedList = retryOnStillPopulating( new Callable()
+ {
+ @Override
+ public String call()
+ {
+ return gen.get()
+ .expectedStatus( 200 )
+ .payload( createJsonFrom( definition ) )
+ .get( getSchemaIndexLabelUri( labelName ) )
+ .entity();
+ }
+ } );
+
+ Map index = new HashMap<>();
+ index.put( "label", labelName );
+ index.put( "property_keys", singletonList( propertyKey ) );
+
+ assertThat( serializedList, hasItem( index ) );
+ }
+
+ private List> retryOnStillPopulating( Callable callable ) throws Exception
+ {
+ long endTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis( 1 );
+ List> serializedList;
+ do
+ {
+ String result = callable.call();
+ serializedList = jsonToList( result );
+ if ( System.currentTimeMillis() > endTime )
+ {
+ fail( "Indexes didn't populate correctly, last result '" + result + "'" );
+ }
+ }
+ while ( stillPopulating( serializedList ) );
+ return serializedList;
+ }
+
+ private boolean stillPopulating( List> serializedList )
+ {
+ // We've created an index. That HTTP call for creating the index will return
+ // immediately and indexing continue in the background. Querying the index endpoint
+ // while index is populating gives back additional information like population progress.
+ // This test below will look at the response of a "get index" result and if still populating
+ // then return true so that caller may retry the call later.
+ for ( Map map : serializedList )
+ {
+ if ( map.containsKey( "population_progress" ) )
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings( "unchecked" )
+ @Documented( "Get all indexes." )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void get_indexes() throws Exception
+ {
+ data.get();
+
+ String labelName1 = labels.newInstance(), propertyKey1 = properties.newInstance();
+ String labelName2 = labels.newInstance(), propertyKey2 = properties.newInstance();
+ createIndex( labelName1, propertyKey1 );
+ createIndex( labelName2, propertyKey2 );
+
+ List> serializedList = retryOnStillPopulating( new Callable()
+ {
+ @Override
+ public String call() throws Exception
+ {
+ return gen.get().expectedStatus( 200 ).get( getSchemaIndexUri() ).entity();
+ }
+ } );
+
+ Map index1 = new HashMap<>();
+ index1.put( "label", labelName1 );
+ index1.put( "property_keys", singletonList( propertyKey1 ) );
+
+ Map index2 = new HashMap<>();
+ index2.put( "label", labelName2 );
+ index2.put( "property_keys", singletonList( propertyKey2 ) );
+
+ assertThat( serializedList, hasItems( index1, index2 ) );
+ }
+
+ @Documented( "Drop index" )
+ @Test
+ @GraphDescription.Graph( nodes = {} )
+ public void drop_index() throws Exception
+ {
+ data.get();
+
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ IndexDefinition schemaIndex = createIndex( labelName, propertyKey );
+ assertThat( Neo4jMatchers.getIndexes( graphdb(), label( labelName ) ), containsOnly( schemaIndex ) );
+
+ gen.get()
+ .expectedStatus( 204 )
+ .delete( getSchemaIndexLabelPropertyUri( labelName, propertyKey ) )
+ .entity();
+
+ assertThat( Neo4jMatchers.getIndexes( graphdb(), label( labelName ) ), not( containsOnly( schemaIndex ) ) );
+ }
+
+ /**
+ * Create an index for a label and property key which already exists.
+ */
+ @Test
+ public void create_existing_index()
+ {
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+ createIndex( labelName, propertyKey );
+ Map definition = map( "property_keys", singletonList( propertyKey ) );
+
+ gen.get()
+ .expectedStatus( 409 )
+ .payload( createJsonFrom( definition ) )
+ .post( getSchemaIndexLabelUri( labelName ) );
+ }
+
+ @Test
+ public void drop_non_existent_index() throws Exception
+ {
+ String labelName = labels.newInstance(), propertyKey = properties.newInstance();
+
+ gen.get()
+ .expectedStatus( 404 )
+ .delete( getSchemaIndexLabelPropertyUri( labelName, propertyKey ) );
+ }
+
+ /**
+ * Creating a compound index should not yet be supported
+ */
+ @Test
+ public void create_compound_index()
+ {
+ Map definition = map( "property_keys", asList( properties.newInstance(), properties.newInstance()) );
+
+ gen.get()
+ .expectedStatus( 400 )
+ .payload( createJsonFrom( definition ) )
+ .post( getSchemaIndexLabelUri( labels.newInstance() ) );
+ }
+
+ private IndexDefinition createIndex( String labelName, String propertyKey )
+ {
+ try ( Transaction tx = graphdb().beginTx() )
+ {
+ IndexDefinition indexDefinition = graphdb().schema().indexFor( label( labelName ) ).on( propertyKey )
+ .create();
+ tx.success();
+ return indexDefinition;
+ }
+ }
+
+ private final Factory labels = UniqueStrings.withPrefix( "label" );
+ private final Factory properties = UniqueStrings.withPrefix( "property" );
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/SetNodePropertiesIT.java b/community/server/src/test/java/org/neo4j/server/rest/SetNodePropertiesIT.java
new file mode 100644
index 0000000000000..ee8db5ab8d37d
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/SetNodePropertiesIT.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.helpers.collection.MapUtil;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.NODE;
+import org.neo4j.test.GraphDescription.PROP;
+
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import static org.neo4j.graphdb.Neo4jMatchers.hasProperty;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+
+public class SetNodePropertiesIT extends
+ AbstractRestFunctionalTestBase
+{
+
+ @Graph( "jim knows joe" )
+ @Documented( "Update node properties.\n" +
+ "\n" +
+ "This will replace all existing properties on the node with the new set\n" +
+ "of attributes." )
+ @Test
+ public void shouldReturn204WhenPropertiesAreUpdated()
+ throws JsonParseException
+ {
+ Node jim = data.get().get( "jim" );
+ assertThat( jim, inTx(graphdb(), not( hasProperty( "age" ) ) ) );
+ gen.get().payload(
+ JsonHelper.createJsonFrom( MapUtil.map( "age", "18" ) ) ).expectedStatus(
+ 204 ).put( getPropertiesUri( jim ) );
+ assertThat( jim, inTx(graphdb(), hasProperty( "age" ).withValue( "18" ) ) );
+ }
+
+ @Graph( "jim knows joe" )
+ @Test
+ public void set_node_properties_in_Unicode()
+ throws JsonParseException
+ {
+ Node jim = data.get().get( "jim" );
+ gen.get().payload(
+ JsonHelper.createJsonFrom( MapUtil.map( "name", "\u4f8b\u5b50" ) ) ).expectedStatus(
+ 204 ).put( getPropertiesUri( jim ) );
+ assertThat( jim, inTx( graphdb(), hasProperty( "name" ).withValue( "\u4f8b\u5b50" ) ) );
+ }
+
+ @Test
+ @Graph( "jim knows joe" )
+ public void shouldReturn400WhenSendinIncompatibleJsonProperties()
+ throws JsonParseException
+ {
+ Map map = new HashMap();
+ map.put( "jim", new HashMap() );
+ gen.get().payload( JsonHelper.createJsonFrom( map ) ).expectedStatus(
+ 400 ).put( getPropertiesUri( data.get().get( "jim" ) ) );
+ }
+
+ @Test
+ @Graph( "jim knows joe" )
+ public void shouldReturn400WhenSendingCorruptJsonProperties()
+ {
+ JaxRsResponse response = RestRequest.req().put(
+ getPropertiesUri( data.get().get( "jim" ) ),
+ "this:::Is::notJSON}" );
+ assertEquals( 400, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ @Graph( "jim knows joe" )
+ public void shouldReturn404WhenPropertiesSentToANodeWhichDoesNotExist()
+ throws JsonParseException
+ {
+ gen.get().payload(
+ JsonHelper.createJsonFrom( MapUtil.map( "key", "val" ) ) ).expectedStatus(
+ 404 ).put( getDataUri() + "node/12345/properties" );
+ }
+
+ private URI getPropertyUri( Node node, String key ) throws Exception
+ {
+ return new URI( getPropertiesUri( node ) + "/" + key );
+ }
+
+ @Documented( "Set property on node.\n" +
+ "\n" +
+ "Setting different properties will retain the existing ones for this node.\n" +
+ "Note that a single value are submitted not as a map but just as a value\n" +
+ "(which is valid JSON) like in the example\n" +
+ "below." )
+ @Graph( nodes = {@NODE(name="jim", properties={@PROP(key="foo2", value="bar2")})} )
+ @Test
+ public void shouldReturn204WhenPropertyIsSet() throws Exception
+ {
+ Node jim = data.get().get( "jim" );
+ gen.get().payload( JsonHelper.createJsonFrom( "bar" ) ).expectedStatus(
+ 204 ).put( getPropertyUri( jim, "foo" ).toString() );
+ assertThat( jim, inTx(graphdb(), hasProperty( "foo" ) ) );
+ assertThat( jim, inTx(graphdb(), hasProperty( "foo2" ) ) );
+ }
+
+ @Documented( "Property values can not be nested.\n" +
+ "\n" +
+ "Nesting properties is not supported. You could for example store the\n" +
+ "nested JSON as a string instead." )
+ @Test
+ public void shouldReturn400WhenSendinIncompatibleJsonProperty()
+ throws Exception
+ {
+ gen.get()
+ .payload( "{\"foo\" : {\"bar\" : \"baz\"}}" )
+ .expectedStatus(
+ 400 ).post( getDataUri() + "node/" );
+ }
+
+ @Test
+ @Graph( "jim knows joe" )
+ public void shouldReturn400WhenSendingCorruptJsonProperty()
+ throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().put(
+ getPropertyUri( data.get().get( "jim" ), "foo" ),
+ "this:::Is::notJSON}" );
+ assertEquals( 400, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ @Graph( "jim knows joe" )
+ public void shouldReturn404WhenPropertySentToANodeWhichDoesNotExist()
+ throws Exception
+ {
+ JaxRsResponse response = RestRequest.req().put(
+ getDataUri() + "node/1234/foo",
+ JsonHelper.createJsonFrom( "bar" ) );
+ assertEquals( 404, response.getStatus() );
+ response.close();
+ }
+
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/SetRelationshipPropertiesIT.java b/community/server/src/test/java/org/neo4j/server/rest/SetRelationshipPropertiesIT.java
new file mode 100644
index 0000000000000..c357394745737
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/SetRelationshipPropertiesIT.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.domain.GraphDbHelper;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription.Graph;
+
+import static org.junit.Assert.assertEquals;
+
+public class SetRelationshipPropertiesIT extends AbstractRestFunctionalDocTestBase
+{
+ private URI propertiesUri;
+ private URI badUri;
+
+ private static FunctionalTestHelper functionalTestHelper;
+
+ @BeforeClass
+ public static void setupServer() throws IOException
+ {
+ functionalTestHelper = new FunctionalTestHelper( server() );
+ }
+
+ @Before
+ public void setupTheDatabase() throws Exception
+ {
+ long relationshipId = new GraphDbHelper( server().getDatabase() ).createRelationship( "KNOWS" );
+ propertiesUri = new URI( functionalTestHelper.relationshipPropertiesUri( relationshipId ) );
+ badUri = new URI( functionalTestHelper.relationshipPropertiesUri( relationshipId + 1 * 99999 ) );
+ }
+
+ @Documented( "Update relationship properties." )
+ @Test
+ @Graph
+ public void shouldReturn204WhenPropertiesAreUpdated() throws JsonParseException
+ {
+ data.get();
+ Map map = new HashMap();
+ map.put( "jim", "tobias" );
+ gen.get().description( startGraph( "update relationship properties" ) )
+ .payload( JsonHelper.createJsonFrom( map ) )
+ .expectedStatus( 204 )
+ .put( propertiesUri.toString() );
+ JaxRsResponse response = updatePropertiesOnServer(map);
+ assertEquals( 204, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn400WhenSendinIncompatibleJsonProperties() throws JsonParseException
+ {
+ Map map = new HashMap();
+ map.put( "jim", new HashMap() );
+ JaxRsResponse response = updatePropertiesOnServer(map);
+ assertEquals( 400, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn400WhenSendingCorruptJsonProperties() {
+ JaxRsResponse response = RestRequest.req().put(propertiesUri.toString(), "this:::Is::notJSON}");
+ assertEquals(400, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn404WhenPropertiesSentToANodeWhichDoesNotExist() throws JsonParseException {
+ Map map = new HashMap();
+ map.put("jim", "tobias");
+
+ JaxRsResponse response = RestRequest.req().put(badUri.toString(), JsonHelper.createJsonFrom(map));
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ private JaxRsResponse updatePropertiesOnServer(final Map map) throws JsonParseException
+ {
+ return RestRequest.req().put(propertiesUri.toString(), JsonHelper.createJsonFrom(map));
+ }
+
+ private String getPropertyUri(final String key) throws Exception
+ {
+ return propertiesUri.toString() + "/" + key ;
+ }
+
+ @Test
+ public void shouldReturn204WhenPropertyIsSet() throws Exception
+ {
+ JaxRsResponse response = setPropertyOnServer("foo", "bar");
+ assertEquals( 204, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn400WhenSendinIncompatibleJsonProperty() throws Exception
+ {
+ JaxRsResponse response = setPropertyOnServer("jim", new HashMap());
+ assertEquals( 400, response.getStatus() );
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn400WhenSendingCorruptJsonProperty() throws Exception {
+ JaxRsResponse response = RestRequest.req().put(getPropertyUri("foo"), "this:::Is::notJSON}");
+ assertEquals(400, response.getStatus());
+ response.close();
+ }
+
+ @Test
+ public void shouldReturn404WhenPropertySentToANodeWhichDoesNotExist() throws Exception {
+ JaxRsResponse response = RestRequest.req().put(badUri.toString() + "/foo", JsonHelper.createJsonFrom("bar"));
+ assertEquals(404, response.getStatus());
+ response.close();
+ }
+
+ private JaxRsResponse setPropertyOnServer(final String key, final Object value) throws Exception {
+ return RestRequest.req().put(getPropertyUri(key), JsonHelper.createJsonFrom(value));
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/TraverserIT.java b/community/server/src/test/java/org/neo4j/server/rest/TraverserIT.java
new file mode 100644
index 0000000000000..61c33c1c60345
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/TraverserIT.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.ws.rs.core.Response.Status;
+
+import org.junit.Test;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.test.GraphDescription.Graph;
+import org.neo4j.test.GraphDescription.NODE;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import static org.neo4j.helpers.collection.MapUtil.map;
+import static org.neo4j.server.rest.domain.JsonHelper.createJsonFrom;
+import static org.neo4j.server.rest.domain.JsonHelper.readJson;
+
+public class TraverserIT extends AbstractRestFunctionalTestBase
+{
+
+ @Test
+ public void shouldGet404WhenTraversingFromNonExistentNode()
+ {
+ gen().expectedStatus( Status.NOT_FOUND.getStatusCode() ).payload(
+ "{}" ).post( getDataUri() + "node/10000/traverse/node" ).entity();
+ }
+
+ @Test
+ @Graph( nodes = {@NODE(name="I")} )
+ public void shouldGet200WhenNoHitsFromTraversing()
+ {
+ assertSize( 0,gen().expectedStatus( 200 ).payload( "" ).post(
+ getTraverseUriNodes( getNode( "I" ) ) ).entity());
+ }
+
+ /**
+ * In order to return relationships,
+ * simply specify the return type as part of the URL.
+ */
+ @Test
+ @Graph( {"I know you", "I own car"} )
+ public void return_relationships_from_a_traversal()
+ {
+ assertSize( 2, gen().expectedStatus( 200 ).payload( "{\"order\":\"breadth_first\",\"uniqueness\":\"none\",\"return_filter\":{\"language\":\"builtin\",\"name\":\"all\"}}" ).post(
+ getTraverseUriRelationships( getNode( "I" ) ) ).entity());
+ }
+
+
+ /**
+ * In order to return paths from a traversal,
+ * specify the +Path+ return type as part of the URL.
+ */
+ @Test
+ @Graph( {"I know you", "I own car"} )
+ public void return_paths_from_a_traversal()
+ {
+ assertSize( 3, gen().expectedStatus( 200 ).payload( "{\"order\":\"breadth_first\",\"uniqueness\":\"none\",\"return_filter\":{\"language\":\"builtin\",\"name\":\"all\"}}" ).post(
+ getTraverseUriPaths( getNode( "I" ) ) ).entity());
+ }
+
+
+ private String getTraverseUriRelationships( Node node )
+ {
+ return getNodeUri( node) + "/traverse/relationship";
+ }
+ private String getTraverseUriPaths( Node node )
+ {
+ return getNodeUri( node) + "/traverse/path";
+ }
+
+ private String getTraverseUriNodes( Node node )
+ {
+ // TODO Auto-generated method stub
+ return getNodeUri( node) + "/traverse/node";
+ }
+
+ @Test
+ @Graph( "I know you" )
+ public void shouldGetSomeHitsWhenTraversingWithDefaultDescription()
+ throws JsonParseException
+ {
+ String entity = gen().expectedStatus( Status.OK.getStatusCode() ).payload( "{}" ).post(
+ getTraverseUriNodes( getNode( "I" ) ) ).entity();
+
+ expectNodes( entity, getNode( "you" ));
+ }
+
+ private void expectNodes( String entity, Node... nodes )
+ throws JsonParseException
+ {
+ Set expected = new HashSet<>();
+ for ( Node node : nodes )
+ {
+ expected.add( getNodeUri( node ) );
+ }
+ Collection> items = (Collection>) readJson( entity );
+ for ( Object item : items )
+ {
+ Map, ?> map = (Map, ?>) item;
+ String uri = (String) map.get( "self" );
+ assertTrue( uri + " not found", expected.remove( uri ) );
+ }
+ assertTrue( "Expected not empty:" + expected, expected.isEmpty() );
+ }
+
+ @Documented( "Traversal using a return filter.\n" +
+ "\n" +
+ "In this example, the +none+ prune evaluator is used and a return filter\n" +
+ "is supplied in order to return all names containing \"t\".\n" +
+ "The result is to be returned as nodes and the max depth is\n" +
+ "set to 3." )
+ @Graph( {"Root knows Mattias", "Root knows Johan", "Johan knows Emil", "Emil knows Peter", "Emil knows Tobias", "Tobias loves Sara"} )
+ @Test
+ public void shouldGetExpectedHitsWhenTraversingWithDescription()
+ throws JsonParseException
+ {
+ Node start = getNode( "Root" );
+ List> rels = new ArrayList<>();
+ rels.add( map( "type", "knows", "direction", "all" ) );
+ rels.add( map( "type", "loves", "direction", "all" ) );
+ String description = createJsonFrom( map(
+ "order",
+ "breadth_first",
+ "uniqueness",
+ "node_global",
+ "prune_evaluator",
+ map( "language", "javascript", "body", "position.length() > 10" ),
+ "return_filter",
+ map( "language", "javascript", "body",
+ "position.endNode().getProperty('name').toLowerCase().contains('t')" ),
+ "relationships", rels, "max_depth", 3 ) );
+ String entity = gen().expectedStatus( 200 ).payload( description ).post(
+ getTraverseUriNodes( start ) ).entity();
+ expectNodes( entity, getNodes( "Root", "Mattias", "Peter", "Tobias" ) );
+ }
+
+ @Documented( "Traversal returning nodes below a certain depth.\n" +
+ "\n" +
+ "Here, all nodes at a traversal depth below 3 are returned." )
+ @Graph( {"Root knows Mattias", "Root knows Johan", "Johan knows Emil", "Emil knows Peter", "Emil knows Tobias", "Tobias loves Sara"} )
+ @Test
+ public void shouldGetExpectedHitsWhenTraversingAtDepth()
+ throws JsonParseException
+ {
+ Node start = getNode( "Root" );
+ String description = createJsonFrom( map(
+ "prune_evaluator",
+ map( "language", "builtin", "name", "none" ),
+ "return_filter",
+ map( "language", "javascript", "body",
+ "position.length()<3;" ) ) );
+ String entity = gen().expectedStatus( 200 ).payload( description ).post(
+ getTraverseUriNodes( start ) ).entity();
+ expectNodes( entity, getNodes( "Root", "Mattias", "Johan", "Emil" ) );
+ }
+
+ @Test
+ @Graph( "I know you" )
+ public void shouldGet400WhenSupplyingInvalidTraverserDescriptionFormat()
+ {
+ gen().expectedStatus( Status.BAD_REQUEST.getStatusCode() ).payload(
+ "::not JSON{[ at all" ).post(
+ getTraverseUriNodes( getNode( "I" ) ) ).entity();
+ }
+
+ @Test
+ @Graph( {"Root knows Mattias",
+ "Root knows Johan", "Johan knows Emil", "Emil knows Peter",
+ "Root eats Cork", "Cork hates Root",
+ "Root likes Banana", "Banana is_a Fruit"} )
+ public void shouldAllowTypeOrderedTraversals()
+ throws JsonParseException
+ {
+ Node start = getNode( "Root" );
+ String description = createJsonFrom( map(
+ "expander", "order_by_type",
+ "relationships",
+ new Map[]{
+ map( "type", "eats"),
+ map( "type", "knows" ),
+ map( "type", "likes" )
+ },
+ "prune_evaluator",
+ map( "language", "builtin",
+ "name", "none" ),
+ "return_filter",
+ map( "language", "javascript",
+ "body", "position.length()<2;" )
+ ) );
+ @SuppressWarnings( "unchecked" )
+ List> nodes = (List>) readJson( gen().expectedStatus( 200 ).payload(
+ description ).post(
+ getTraverseUriNodes( start ) ).entity() );
+
+ assertThat( nodes.size(), is( 5 ) );
+ assertThat( getName( nodes.get( 0 ) ), is( "Root" ) );
+ assertThat( getName( nodes.get( 1 ) ), is( "Cork" ) );
+
+ // We don't really care about the ordering between Johan and Mattias, we just assert that they
+ // both are there, in between Root/Cork and Banana
+ Set knowsNodes = new HashSet<>( Arrays.asList( "Johan", "Mattias" ) );
+ assertTrue( knowsNodes.remove( getName( nodes.get( 2 ) ) ) );
+ assertTrue( knowsNodes.remove( getName( nodes.get( 3 ) ) ) );
+
+ assertThat( getName( nodes.get( 4 ) ), is( "Banana" ) );
+ }
+
+ @SuppressWarnings( "unchecked" )
+ private String getName( Map propContainer )
+ {
+ return (String) ((Map)propContainer.get( "data" )).get( "name" );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/paging/PagedTraverserIT.java b/community/server/src/test/java/org/neo4j/server/rest/paging/PagedTraverserIT.java
new file mode 100644
index 0000000000000..195c6535cfb72
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/paging/PagedTraverserIT.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest.paging;
+
+import java.net.URI;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.MediaType;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.RelationshipType;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.FakeClock;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.database.Database;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.helpers.ServerHelper;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.RESTDocsGenerator;
+import org.neo4j.server.rest.RESTDocsGenerator.ResponseEntity;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.scripting.javascript.GlobalJavascriptInitializer;
+import org.neo4j.test.TestData;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import static org.neo4j.test.SuppressOutput.suppressAll;
+
+public class PagedTraverserIT extends ExclusiveServerTestBase
+{
+ private static CommunityNeoServer server;
+ private static FunctionalTestHelper functionalTestHelper;
+
+ private Node theStartNode;
+ private static final String PAGED_TRAVERSE_LINK_REL = "paged_traverse";
+ private static final int SHORT_LIST_LENGTH = 33;
+ private static final int LONG_LIST_LENGTH = 444;
+ private static final int VERY_LONG_LIST_LENGTH = LONG_LIST_LENGTH*2;
+
+ @ClassRule
+ public static TemporaryFolder staticFolder = new TemporaryFolder();
+
+ public
+ @Rule
+ TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+ private static FakeClock clock;
+
+ @Before
+ public void setUp()
+ {
+ gen.get().setSection( "dev/rest-api" );
+ }
+
+ @BeforeClass
+ public static void setupServer() throws Exception
+ {
+ clock = new FakeClock();
+ server = CommunityServerBuilder.server()
+ .usingDataDir( staticFolder.getRoot().getAbsolutePath() )
+ .withClock( clock )
+ .build();
+
+ suppressAll().call( (Callable) () -> {
+ server.start();
+ return null;
+ } );
+ functionalTestHelper = new FunctionalTestHelper( server );
+ }
+
+ @Before
+ public void setupTheDatabase() throws Exception
+ {
+ ServerHelper.cleanTheDatabase( server );
+ }
+
+ @AfterClass
+ public static void stopServer() throws Exception
+ {
+ suppressAll().call( (Callable) () -> {
+ server.stop();
+ return null;
+ } );
+ }
+
+ @Test
+ public void nodeRepresentationShouldHaveLinkToPagedTraverser() throws Exception
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ JaxRsResponse response = RestRequest.req().get( functionalTestHelper.nodeUri( theStartNode.getId() ) );
+
+ Map jsonMap = JsonHelper.jsonToMap( response.getEntity() );
+
+ assertNotNull( jsonMap.containsKey( PAGED_TRAVERSE_LINK_REL ) );
+ assertThat( String.valueOf( jsonMap.get( PAGED_TRAVERSE_LINK_REL ) ),
+ containsString( "/db/data/node/" + String.valueOf( theStartNode.getId() )
+ + "/paged/traverse/{returnType}{?pageSize,leaseTime}" ) );
+ }
+
+ @Documented( "Creating a paged traverser.\n\n" +
+ "Paged traversers are created by ++POST++-ing a\n" +
+ "traversal description to the link identified by the +paged_traverser+ key\n" +
+ "in a node representation. When creating a paged traverser, the same\n" +
+ "options apply as for a regular traverser, meaning that +node+, +path+,\n" +
+ "or +fullpath+, can be targeted." )
+ @Test
+ public void shouldPostATraverserWithDefaultOptionsAndReceiveTheFirstPageOfResults() throws Exception
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ ResponseEntity entity = gen.get()
+ .expectedType( MediaType.valueOf( "application/json; charset=UTF-8" ) )
+ .expectedHeader( "Location" )
+ .expectedStatus( 201 )
+ .payload( traverserDescription() )
+ .payloadType( MediaType.APPLICATION_JSON_TYPE )
+ .post( functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node" );
+ assertEquals( 201, entity.response()
+ .getStatus() );
+ assertThat( entity.response()
+ .getLocation()
+ .toString(), containsString( "/db/data/node/" + theStartNode.getId() + "/paged/traverse/node/" ) );
+ assertEquals( "application/json; charset=UTF-8", entity.response()
+ .getType()
+ .toString() );
+ }
+
+ @Documented( "Paging through the results of a paged traverser.\n\n" +
+ "Paged traversers holdstate on the server, and allow clients to page through\n" +
+ "the results of a traversal. To progress to the next page of traversal results,\n" +
+ "the client issues a HTTP GET request on the paged traversal URI which causes the\n" +
+ "traversal to fill the next page (or partially fill it if insufficient\n" +
+ "results are available).\n" +
+ " \n" +
+ "Note that if a traverser expires through inactivity it will cause a 404\n" +
+ "response on the next +GET+ request. Traversers' leases are renewed on\n" +
+ "every successful access for the same amount of time as originally\n" +
+ "specified.\n" +
+ " \n" +
+ "When the paged traverser reaches the end of its results, the client can\n" +
+ "expect a 404 response as the traverser is disposed by the server." )
+ @Test
+ public void shouldBeAbleToTraverseAllThePagesWithDefaultPageSize()
+ {
+ theStartNode = createLinkedList( LONG_LIST_LENGTH, server.getDatabase() );
+
+ URI traverserLocation = createPagedTraverser().getLocation();
+
+ int enoughPagesToExpireTheTraverser = 3;
+ for ( int i = 0; i < enoughPagesToExpireTheTraverser; i++ )
+ {
+
+ gen.get()
+ .expectedType( MediaType.APPLICATION_JSON_TYPE )
+ .expectedStatus( 200 )
+ .payload( traverserDescription() )
+ .get( traverserLocation.toString() );
+ }
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Test
+ public void shouldExpireTheTraverserAfterDefaultTimeoutAndGetA404Response()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ JaxRsResponse postResponse = createPagedTraverser();
+ assertEquals( 201, postResponse.getStatus() );
+
+ final int TEN_MINUTES = 10;
+ clock.forward( TEN_MINUTES, TimeUnit.MINUTES );
+
+ JaxRsResponse getResponse = new RestRequest( postResponse.getLocation() ).get();
+
+ assertEquals( 404, getResponse.getStatus() );
+ }
+
+ @Documented( "Paged traverser page size.\n\n" +
+ "The default page size is 50 items, but\n" +
+ "depending on the application larger or smaller pages sizes might be\n" +
+ "appropriate. This can be set by adding a +pageSize+ query parameter." )
+ @Test
+ public void shouldBeAbleToTraverseAllThePagesWithNonDefaultPageSize()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ URI traverserLocation = createPagedTraverserWithPageSize( 1 ).getLocation();
+
+ int enoughPagesToExpireTheTraverser = 12;
+ for ( int i = 0; i < enoughPagesToExpireTheTraverser; i++ )
+ {
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 200, response.getStatus() );
+ }
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Documented( "Paged traverser timeout.\n\n" +
+ "The default timeout for a paged traverser is 60\n" +
+ "seconds, but depending on the application larger or smaller timeouts\n" +
+ "might be appropriate. This can be set by adding a +leaseTime+ query\n" +
+ "parameter with the number of seconds the paged traverser should last." )
+ @Test
+ public void shouldExpireTraverserWithNonDefaultTimeout()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ URI traverserLocation = createPagedTraverserWithTimeoutInMinutes( 10 ).getLocation();
+
+ clock.forward( 11, TimeUnit.MINUTES );
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Test
+ public void shouldTraverseAllPagesWithANonDefaultTimeoutAndNonDefaultPageSize()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ URI traverserLocation = createPagedTraverserWithTimeoutInMinutesAndPageSize( 10, 2 ).getLocation();
+
+ int enoughPagesToExpireTheTraverser = 6;
+ for ( int i = 0; i < enoughPagesToExpireTheTraverser; i++ )
+ {
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 200, response.getStatus() );
+ }
+
+ JaxRsResponse response = new RestRequest( traverserLocation ).get();
+ assertEquals( 404, response.getStatus() );
+ }
+
+ @Test
+ public void shouldRespondWith400OnNegativeLeaseTime()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ int negativeLeaseTime = -9;
+ JaxRsResponse response = RestRequest.req().post(
+ functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?leaseTime=" +
+ String.valueOf( negativeLeaseTime ), traverserDescription() );
+
+ assertEquals( 400, response.getStatus() );
+ }
+
+ @Test
+ public void shouldRespondWith400OnNegativePageSize()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ int negativePageSize = -99;
+ JaxRsResponse response = RestRequest.req().post(
+ functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?pageSize=" +
+ String.valueOf( negativePageSize ), traverserDescription() );
+
+ assertEquals( 400, response.getStatus() );
+ }
+
+
+ @Test
+ public void shouldRespondWith400OnScriptErrors()
+ {
+ GlobalJavascriptInitializer.initialize( GlobalJavascriptInitializer.Mode.SANDBOXED );
+
+ theStartNode = createLinkedList( 1, server.getDatabase() );
+
+ JaxRsResponse response = RestRequest.req().post(
+ functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?pageSize=50",
+ "{"
+ + "\"prune_evaluator\":{\"language\":\"builtin\",\"name\":\"none\"},"
+ + "\"return_filter\":{\"language\":\"javascript\",\"body\":\"position.getClass()" +
+ ".getClassLoader();\"},"
+ + "\"order\":\"depth_first\","
+ + "\"relationships\":{\"type\":\"NEXT\",\"direction\":\"out\"}"
+ + "}" );
+
+ assertEquals( 400, response.getStatus() );
+ }
+
+ @Test
+ public void shouldRespondWith200OnFirstDeletionOfTraversalAnd404Afterwards()
+ {
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ JaxRsResponse response = createPagedTraverser();
+
+ final RestRequest request = RestRequest.req();
+ JaxRsResponse deleteResponse = request.delete( response.getLocation() );
+ assertEquals( 200, deleteResponse.getStatus() );
+
+ deleteResponse = request.delete( response.getLocation() );
+ assertEquals( 404, deleteResponse.getStatus() );
+ }
+
+ @Test
+ public void shouldAcceptJsonAndStreamingFlagAndProduceStreamedJson()
+ {
+ // given
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+
+ // when
+ JaxRsResponse pagedTraverserResponse = createStreamingPagedTraverserWithTimeoutInMinutesAndPageSize( 60, 1 );
+
+
+ System.out.println( pagedTraverserResponse.getHeaders().getFirst( "Content-Type" ) );
+
+ // then
+ assertNotNull( pagedTraverserResponse.getHeaders().getFirst( "Content-Type" ) );
+ assertThat( pagedTraverserResponse.getHeaders().getFirst( "Content-Type" ),
+ containsString( "application/json; charset=UTF-8; stream=true" ) );
+ }
+
+ private JaxRsResponse createStreamingPagedTraverserWithTimeoutInMinutesAndPageSize( int leaseTimeInSeconds,
+ int pageSize )
+ {
+ String description = traverserDescription();
+
+ return RestRequest.req().header( "X-Stream", "true" ).post(
+ functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?leaseTime="
+ + leaseTimeInSeconds + "&pageSize=" + pageSize, description );
+ }
+
+ @Test
+ public void should201WithAcceptJsonHeader()
+ {
+ // given
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+ String uri = functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node";
+
+ // when
+ JaxRsResponse response = RestRequest.req().accept( MediaType.APPLICATION_JSON_TYPE ).post( uri,
+ traverserDescription() );
+
+ // then
+ assertEquals( 201, response.getStatus() );
+ assertNotNull( response.getHeaders().getFirst( "Content-Type" ) );
+ assertThat( response.getType().toString(), containsString( MediaType.APPLICATION_JSON ) );
+ }
+
+ @Test
+ public void should201WithAcceptHtmlHeader()
+ {
+ // given
+ theStartNode = createLinkedList( SHORT_LIST_LENGTH, server.getDatabase() );
+ String uri = functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node";
+
+ // when
+ JaxRsResponse response = RestRequest.req().accept( MediaType.TEXT_HTML_TYPE ).post( uri,
+ traverserDescription() );
+
+ // then
+ assertEquals( 201, response.getStatus() );
+ assertNotNull( response.getHeaders().getFirst( "Content-Type" ) );
+ assertThat( response.getType().toString(), containsString( MediaType.TEXT_HTML ) );
+ }
+
+ @Test
+ public void shouldHaveTransportEncodingChunkedOnResponseHeader()
+ {
+ // given
+ theStartNode = createLinkedList( VERY_LONG_LIST_LENGTH, server.getDatabase() );
+
+ // when
+ JaxRsResponse response = createStreamingPagedTraverserWithTimeoutInMinutesAndPageSize( 60, 1000 );
+
+ // then
+ assertEquals( 201, response.getStatus() );
+ assertEquals( "application/json; charset=UTF-8; stream=true", response.getHeaders().getFirst( "Content-Type"
+ ) );
+ assertThat( response.getHeaders().getFirst( "Transfer-Encoding" ), containsString( "chunked" ) );
+ }
+
+ private JaxRsResponse createPagedTraverserWithTimeoutInMinutesAndPageSize( final int leaseTimeInSeconds,
+ final int pageSize )
+ {
+ String description = traverserDescription();
+
+ return RestRequest.req().post(
+ functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?leaseTime="
+ + leaseTimeInSeconds + "&pageSize=" + pageSize, description );
+ }
+
+ private JaxRsResponse createPagedTraverserWithTimeoutInMinutes( final int leaseTime )
+ {
+ ResponseEntity responseEntity = gen.get()
+ .expectedType( MediaType.APPLICATION_JSON_TYPE )
+ .expectedStatus( 201 )
+ .payload( traverserDescription() )
+ .post( functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?leaseTime="
+ + String.valueOf( leaseTime ) );
+
+ return responseEntity.response();
+ }
+
+ private JaxRsResponse createPagedTraverserWithPageSize( final int pageSize )
+ {
+ ResponseEntity responseEntity = gen.get()
+ .expectedType( MediaType.APPLICATION_JSON_TYPE )
+ .expectedStatus( 201 )
+ .payload( traverserDescription() )
+ .post( functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node?pageSize="
+ + String.valueOf( pageSize ) );
+
+ return responseEntity.response();
+ }
+
+ private JaxRsResponse createPagedTraverser()
+ {
+ final String uri = functionalTestHelper.nodeUri( theStartNode.getId() ) + "/paged/traverse/node";
+ return RestRequest.req().post( uri, traverserDescription() );
+ }
+
+ private String traverserDescription()
+ {
+ String description = "{"
+ + "\"prune_evaluator\":{\"language\":\"builtin\",\"name\":\"none\"},"
+ + "\"return_filter\":{\"language\":\"javascript\",\"body\":\"position.endNode().getProperty('name')" +
+ ".contains('1');\"},"
+ + "\"order\":\"depth_first\","
+ + "\"relationships\":{\"type\":\"NEXT\",\"direction\":\"out\"}"
+ + "}";
+
+ return description;
+ }
+
+ private Node createLinkedList( final int listLength, final Database db )
+ {
+ Node startNode = null;
+ try ( Transaction tx = db.getGraph().beginTx() )
+ {
+ Node previous = null;
+ for ( int i = 0; i < listLength; i++ )
+ {
+ Node current = db.getGraph().createNode();
+ current.setProperty( "name", String.valueOf( i ) );
+
+ if ( previous != null )
+ {
+ previous.createRelationshipTo( current, RelationshipType.withName( "NEXT" ) );
+ }
+ else
+ {
+ startNode = current;
+ }
+
+ previous = current;
+ }
+ tx.success();
+ return startNode;
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationIT.java b/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationIT.java
new file mode 100644
index 0000000000000..78b30c7f4611e
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/security/AuthenticationIT.java
@@ -0,0 +1,352 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest.security;
+
+import java.io.IOException;
+import javax.ws.rs.core.HttpHeaders;
+
+import com.sun.jersey.core.util.Base64;
+import org.codehaus.jackson.JsonNode;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.neo4j.graphdb.factory.GraphDatabaseSettings;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.rest.RESTDocsGenerator;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.string.UTF8;
+import org.neo4j.test.TestData;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+import org.neo4j.test.server.HTTP;
+import org.neo4j.test.server.HTTP.RawPayload;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class AuthenticationIT extends ExclusiveServerTestBase
+{
+ public @Rule TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+ private CommunityNeoServer server;
+
+ @Before
+ public void setUp()
+ {
+ gen.get().setSection( "dev/rest-api" );
+ }
+
+ @Test
+ @Documented( "Missing authorization\n" +
+ "\n" +
+ "If an +Authorization+ header is not supplied, the server will reply with an error." )
+ public void missing_authorization() throws JsonParseException, IOException
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 401 )
+ .expectedHeader( "WWW-Authenticate", "Basic realm=\"Neo4j\"" )
+ .get( dataURL() );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ JsonNode firstError = data.get( "errors" ).get( 0 );
+ assertThat( firstError.get( "code" ).asText(), equalTo( "Neo.ClientError.Security.Unauthorized" ) );
+ assertThat( firstError.get( "message" ).asText(), equalTo( "No authentication header supplied." ) );
+ }
+
+ @Test
+ @Documented( "Authenticate to access the server\n" +
+ "\n" +
+ "Authenticate by sending a username and a password to Neo4j using HTTP Basic Auth.\n" +
+ "Requests should include an +Authorization+ header, with a value of +Basic +,\n" +
+ "where \"payload\" is a base64 encoded string of \"username:password\"." )
+ public void successful_authentication() throws JsonParseException, IOException
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) )
+ .get( userURL( "neo4j" ) );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ assertThat( data.get( "username" ).asText(), equalTo( "neo4j" ) );
+ assertThat( data.get( "password_change_required" ).asBoolean(), equalTo( false ) );
+ assertThat( data.get( "password_change" ).asText(), equalTo( passwordURL( "neo4j" ) ) );
+ }
+
+ @Test
+ @Documented( "Incorrect authentication\n" +
+ "\n" +
+ "If an incorrect username or password is provided, the server replies with an error." )
+ public void incorrect_authentication() throws JsonParseException, IOException
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 401 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "incorrect" ) )
+ .expectedHeader( "WWW-Authenticate", "Basic realm=\"Neo4j\"" )
+ .post( dataURL() );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ JsonNode firstError = data.get( "errors" ).get( 0 );
+ assertThat( firstError.get( "code" ).asText(), equalTo( "Neo.ClientError.Security.Unauthorized" ) );
+ assertThat( firstError.get( "message" ).asText(), equalTo( "Invalid username or password." ) );
+ }
+
+ @Test
+ @Documented( "Required password changes\n" +
+ "\n" +
+ "In some cases, like the very first time Neo4j is accessed, the user will be required to choose\n" +
+ "a new password. The database will signal that a new password is required and deny access.\n" +
+ "\n" +
+ "See <> for how to set a new password." )
+ public void password_change_required() throws JsonParseException, IOException
+ {
+ // Given
+ startServer( true );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 403 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) )
+ .get( dataURL() );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ JsonNode firstError = data.get( "errors" ).get( 0 );
+ assertThat( firstError.get( "code" ).asText(), equalTo( "Neo.ClientError.Security.Forbidden" ) );
+ assertThat( firstError.get( "message" ).asText(), equalTo( "User is required to change their password." ) );
+ assertThat( data.get( "password_change" ).asText(), equalTo( passwordURL( "neo4j" ) ) );
+ }
+
+ @Test
+ @Documented( "When auth is disabled\n" +
+ "\n" +
+ "When auth has been disabled in the configuration, requests can be sent without an +Authorization+ header." )
+ public void auth_disabled() throws IOException
+ {
+ // Given
+ startServer( false );
+
+ // Document
+ gen.get()
+ .expectedStatus( 200 )
+ .get( dataURL() );
+ }
+
+ @Test
+ public void shouldSayMalformedHeaderIfMalformedAuthorization() throws Exception
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // When
+ HTTP.Response response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, "This makes no sense" ).GET( dataURL() );
+
+ // Then
+ assertThat( response.status(), equalTo( 400 ) );
+ assertThat( response.get( "errors" ).get( 0 ).get( "code" ).asText(), equalTo( "Neo.ClientError.Request.InvalidFormat" ) );
+ assertThat( response.get( "errors" ).get( 0 ).get( "message" ).asText(), equalTo( "Invalid authentication header." ) );
+ }
+
+ @Test
+ public void shouldNotAllowDataAccess() throws Exception
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // When & then
+ assertAuthorizationRequired( "POST", "db/data/node", RawPayload.quotedJson( "{'name':'jake'}" ), 201 );
+ assertAuthorizationRequired( "GET", "db/data/node/1234", 404 );
+ assertAuthorizationRequired( "POST", "db/data/transaction/commit", RawPayload.quotedJson(
+ "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ), 200 );
+
+ assertEquals(200, HTTP.GET( server.baseUri().resolve( "" ).toString() ).status() );
+ }
+
+ @Test
+ public void shouldAllowAllAccessIfAuthenticationIsDisabled() throws Exception
+ {
+ // Given
+ startServer( false );
+
+ // When & then
+ assertEquals( 201, HTTP.POST( server.baseUri().resolve( "db/data/node" ).toString(),
+ RawPayload.quotedJson( "{'name':'jake'}" ) ).status() );
+ assertEquals( 404, HTTP.GET( server.baseUri().resolve( "db/data/node/1234" ).toString() ).status() );
+ assertEquals( 200, HTTP.POST( server.baseUri().resolve( "db/data/transaction/commit" ).toString(),
+ RawPayload.quotedJson( "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ) ).status() );
+ }
+
+ @Test
+ public void shouldReplyNicelyToTooManyFailedAuthAttempts() throws Exception
+ {
+ // Given
+ startServerWithConfiguredUser();
+ long timeout = System.currentTimeMillis() + 30_000;
+
+ // When
+ HTTP.Response response = null;
+ while ( System.currentTimeMillis() < timeout )
+ {
+ // Done in a loop because we're racing with the clock to get enough failed requests into 5 seconds
+ response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "incorrect" ) ).POST(
+ server.baseUri().resolve( "authentication" ).toString(),
+ HTTP.RawPayload.quotedJson( "{'username':'neo4j', 'password':'something that is wrong'}" )
+ );
+
+ if ( response.status() == 429 )
+ {
+ break;
+ }
+ }
+
+ // Then
+ assertThat( response.status(), equalTo( 429 ) );
+ JsonNode firstError = response.get( "errors" ).get( 0 );
+ assertThat( firstError.get( "code" ).asText(), equalTo( "Neo.ClientError.Security.AuthenticationRateLimit" ) );
+ assertThat( firstError.get( "message" ).asText(), equalTo( "Too many failed authentication requests. Please wait 5 seconds and try again." ) );
+ }
+
+ // TODO: Enable this test when we have authorization in place
+ @Test
+ @Ignore
+ public void shouldNotAllowDataAccessForUnauthorizedUser() throws Exception
+ {
+ // Given
+ startServerWithConfiguredUser(); // TODO: The user for this test should not have read access
+
+ // When
+ HTTP.Response response =
+ HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ).POST(
+ server.baseUri().resolve( "authentication" ).toString(),
+ HTTP.RawPayload.quotedJson( "{'username':'neo4j', 'password':'secret'}" )
+ );
+
+ // When & then
+ assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) )
+ .POST( server.baseUri().resolve( "db/data/node" ).toString(),
+ RawPayload.quotedJson( "{'name':'jake'}" ) ).status() );
+ assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) )
+ .GET( server.baseUri().resolve( "db/data/node/1234" ).toString() ).status() );
+ assertEquals( 403, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) )
+ .POST( server.baseUri().resolve( "db/data/transaction/commit" ).toString(),
+ RawPayload.quotedJson( "{'statements':[{'statement':'MATCH (n) RETURN n'}]}" ) ).status() );
+ }
+
+ private void assertAuthorizationRequired( String method, String path, int expectedAuthorizedStatus ) throws JsonParseException
+ {
+ assertAuthorizationRequired( method, path, null, expectedAuthorizedStatus );
+ }
+
+ private void assertAuthorizationRequired( String method, String path, Object payload, int expectedAuthorizedStatus ) throws JsonParseException
+ {
+ // When no header
+ HTTP.Response response = HTTP.request( method, server.baseUri().resolve( path ).toString(), payload );
+ assertThat(response.status(), equalTo(401));
+ assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.Unauthorized"));
+ assertThat(response.get("errors").get(0).get("message").asText(), equalTo("No authentication header supplied."));
+ assertThat(response.header( HttpHeaders.WWW_AUTHENTICATE ), equalTo("Basic realm=\"Neo4j\""));
+
+ // When malformed header
+ response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, "This makes no sense" ).request( method, server.baseUri().resolve( path ).toString(), payload );
+ assertThat(response.status(), equalTo(400));
+ assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Request.InvalidFormat"));
+ assertThat(response.get("errors").get(0).get( "message" ).asText(), equalTo("Invalid authentication header."));
+
+ // When invalid credential
+ response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "incorrect" ) ).request( method, server.baseUri().resolve( path ).toString(), payload );
+ assertThat(response.status(), equalTo(401));
+ assertThat(response.get("errors").get(0).get("code").asText(), equalTo("Neo.ClientError.Security.Unauthorized"));
+ assertThat(response.get("errors").get(0).get("message").asText(), equalTo("Invalid username or password."));
+ assertThat(response.header(HttpHeaders.WWW_AUTHENTICATE ), equalTo("Basic realm=\"Neo4j\""));
+
+ // When authorized
+ response = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ).request( method, server.baseUri().resolve( path ).toString(), payload );
+ assertThat(response.status(), equalTo(expectedAuthorizedStatus));
+ }
+
+ @After
+ public void cleanup()
+ {
+ if(server != null) {server.stop();}
+ }
+
+ public void startServerWithConfiguredUser() throws IOException
+ {
+ startServer( true );
+ // Set the password
+ HTTP.Response post = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ).POST(
+ server.baseUri().resolve( "/user/neo4j/password" ).toString(),
+ RawPayload.quotedJson( "{'password':'secret'}" )
+ );
+ assertEquals( 200, post.status() );
+ }
+
+ public void startServer( boolean authEnabled ) throws IOException
+ {
+ server = CommunityServerBuilder.server()
+ .withProperty( GraphDatabaseSettings.auth_enabled.name(), Boolean.toString( authEnabled ) )
+ .build();
+ server.start();
+ }
+
+ private String challengeResponse( String username, String password )
+ {
+ return "Basic " + base64( username + ":" + password );
+ }
+
+ private String dataURL()
+ {
+ return server.baseUri().resolve( "db/data/" ).toString();
+ }
+
+ private String userURL( String username )
+ {
+ return server.baseUri().resolve( "user/" + username ).toString();
+ }
+
+ private String passwordURL( String username )
+ {
+ return server.baseUri().resolve( "user/" + username + "/password" ).toString();
+ }
+
+ private String base64(String value)
+ {
+ return UTF8.decode( Base64.encode( value ) );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/security/SecurityRulesIT.java b/community/server/src/test/java/org/neo4j/server/rest/security/SecurityRulesIT.java
new file mode 100644
index 0000000000000..cfcbadca00c76
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/security/SecurityRulesIT.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest.security;
+
+import java.net.URI;
+import javax.ws.rs.core.MediaType;
+
+import org.dummy.web.service.DummyThirdPartyWebService;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.helpers.FunctionalTestHelper;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.RESTDocsGenerator;
+import org.neo4j.test.TestData;
+import org.neo4j.test.TestData.Title;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class SecurityRulesIT extends ExclusiveServerTestBase
+{
+ private CommunityNeoServer server;
+
+ private FunctionalTestHelper functionalTestHelper;
+
+ @Rule
+ public TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+
+ @After
+ public void stopServer()
+ {
+ if ( server != null )
+ {
+ server.stop();
+ }
+ }
+
+ @Test
+ @Title( "Enforcing Server Authorization Rules" )
+ @Documented( "In this example, a (dummy) failing security rule is registered to deny\n" +
+ "access to all URIs to the server by listing the rules class in\n" +
+ "'neo4j.conf':\n" +
+ "\n" +
+ "@@config\n" +
+ "\n" +
+ "with the rule source code of:\n" +
+ "\n" +
+ "@@failingRule\n" +
+ "\n" +
+ "With this rule registered, any access to the server will be\n" +
+ "denied. In a production-quality implementation the rule\n" +
+ "will likely lookup credentials/claims in a 3rd-party\n" +
+ "directory service (e.g. LDAP) or in a local database of\n" +
+ "authorized users." )
+ public void should401WithBasicChallengeWhenASecurityRuleFails()
+ throws Exception
+ {
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning().withSecurityRules(
+ PermanentlyFailingSecurityRule.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ gen.get().addSnippet(
+ "config",
+ "\n[source,properties]\n----\ndbms.security.http_authorization_classes=my.rules" +
+ ".PermanentlyFailingSecurityRule\n----\n" );
+ gen.get().addTestSourceSnippets( PermanentlyFailingSecurityRule.class,
+ "failingRule" );
+ functionalTestHelper = new FunctionalTestHelper( server );
+ gen.get().setSection( "ops" );
+ JaxRsResponse response = gen.get().expectedStatus( 401 ).expectedHeader(
+ "WWW-Authenticate" ).post( functionalTestHelper.nodeUri() ).response();
+
+ assertThat( response.getHeaders().getFirst( "WWW-Authenticate" ),
+ containsString( "Basic realm=\""
+ + PermanentlyFailingSecurityRule.REALM + "\"" ) );
+ }
+
+ @Test
+ public void should401WithBasicChallengeIfAnyOneOfTheRulesFails()
+ throws Exception
+ {
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning().withSecurityRules(
+ PermanentlyFailingSecurityRule.class.getCanonicalName(),
+ PermanentlyPassingSecurityRule.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ JaxRsResponse response = gen.get().expectedStatus( 401 ).expectedHeader(
+ "WWW-Authenticate" ).post( functionalTestHelper.nodeUri() ).response();
+
+ assertThat( response.getHeaders().getFirst( "WWW-Authenticate" ),
+ containsString( "Basic realm=\""
+ + PermanentlyFailingSecurityRule.REALM + "\"" ) );
+ }
+
+ @Test
+ public void shouldInvokeAllSecurityRules() throws Exception
+ {
+ // given
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning().withSecurityRules(
+ NoAccessToDatabaseSecurityRule.class.getCanonicalName())
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ // when
+ gen.get().expectedStatus( 401 ).get( functionalTestHelper.dataUri() ).response();
+
+ // then
+ assertTrue( NoAccessToDatabaseSecurityRule.wasInvoked() );
+ }
+
+ @Test
+ public void shouldRespondWith201IfAllTheRulesPassWhenCreatingANode()
+ throws Exception
+ {
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning().withSecurityRules(
+ PermanentlyPassingSecurityRule.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ gen.get().expectedStatus( 201 ).expectedHeader( "Location" ).post(
+ functionalTestHelper.nodeUri() ).response();
+ }
+
+ @Test
+ @Title( "Using Wildcards to Target Security Rules" )
+ @Documented( "In this example, a security rule is registered to deny\n" +
+ "access to all URIs to the server by listing the rule(s) class(es) in\n" +
+ "'neo4j.conf'.\n" +
+ "In this case, the rule is registered\n" +
+ "using a wildcard URI path (where `*` characters can be used to signify\n" +
+ "any part of the path). For example `/users*` means the rule\n" +
+ "will be bound to any resources under the `/users` root path. Similarly\n" +
+ "`/users*type*` will bind the rule to resources matching\n" +
+ "URIs like `/users/fred/type/premium`.\n" +
+ "\n" +
+ "@@config\n" +
+ "\n" +
+ "with the rule source code of:\n" +
+ "\n" +
+ "@@failingRuleWithWildcardPath\n" +
+ "\n" +
+ "With this rule registered, any access to URIs under /protected/ will be\n" +
+ "denied by the server. Using wildcards allows flexible targeting of security rules to\n" +
+ "arbitrary parts of the server's API, including any unmanaged extensions or managed\n" +
+ "plugins that have been registered." )
+ public void aSimpleWildcardUriPathShould401OnAccessToProtectedSubPath()
+ throws Exception
+ {
+ String mountPoint = "/protected/tree/starts/here" + DummyThirdPartyWebService.DUMMY_WEB_SERVICE_MOUNT_POINT;
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning()
+ .withThirdPartyJaxRsPackage( "org.dummy.web.service",
+ mountPoint )
+ .withSecurityRules(
+ PermanentlyFailingSecurityRuleWithWildcardPath.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+
+ gen.get()
+ .addSnippet(
+ "config",
+ "\n[source,properties]\n----\ndbms.security.http_authorization_classes=my.rules" +
+ ".PermanentlyFailingSecurityRuleWithWildcardPath\n----\n" );
+
+ gen.get().addTestSourceSnippets( PermanentlyFailingSecurityRuleWithWildcardPath.class,
+ "failingRuleWithWildcardPath" );
+
+ gen.get().setSection("ops");
+
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ JaxRsResponse clientResponse = gen.get()
+ .expectedStatus( 401 )
+ .expectedType( MediaType.APPLICATION_JSON_TYPE )
+ .expectedHeader( "WWW-Authenticate" )
+ .get( trimTrailingSlash( functionalTestHelper.baseUri() )
+ + mountPoint + "/more/stuff" )
+ .response();
+
+ assertEquals(401, clientResponse.getStatus());
+ }
+
+ @Test
+ @Title( "Using Complex Wildcards to Target Security Rules" )
+ @Documented( "In this example, a security rule is registered to deny\n" +
+ "access to all URIs matching a complex pattern.\n" +
+ "The config looks like this:\n" +
+ "\n" +
+ "@@config\n" +
+ "\n" +
+ "with the rule source code of:\n" +
+ "\n" +
+ "@@failingRuleWithComplexWildcardPath" )
+ public void aComplexWildcardUriPathShould401OnAccessToProtectedSubPath()
+ throws Exception
+ {
+ String mountPoint = "/protected/wildcard_replacement/x/y/z/something/else/more_wildcard_replacement/a/b/c" +
+ "/final/bit";
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning()
+ .withThirdPartyJaxRsPackage( "org.dummy.web.service",
+ mountPoint )
+ .withSecurityRules(
+ PermanentlyFailingSecurityRuleWithComplexWildcardPath.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ gen.get().addSnippet(
+ "config",
+ "\n[source,properties]\n----\ndbms.security.http_authorization_classes=my.rules" +
+ ".PermanentlyFailingSecurityRuleWithComplexWildcardPath\n----\n");
+ gen.get().addTestSourceSnippets( PermanentlyFailingSecurityRuleWithComplexWildcardPath.class,
+ "failingRuleWithComplexWildcardPath" );
+ gen.get().setSection( "ops" );
+
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ JaxRsResponse clientResponse = gen.get()
+ .expectedStatus( 401 )
+ .expectedType( MediaType.APPLICATION_JSON_TYPE )
+ .expectedHeader( "WWW-Authenticate" )
+ .get( trimTrailingSlash( functionalTestHelper.baseUri() )
+ + mountPoint + "/more/stuff" )
+ .response();
+
+ assertEquals( 401, clientResponse.getStatus() );
+ }
+
+
+ @Test
+ public void should403WhenAuthenticatedButForbidden()
+ throws Exception
+ {
+ server = CommunityServerBuilder.server().withDefaultDatabaseTuning().withSecurityRules(
+ PermanentlyForbiddenSecurityRule.class.getCanonicalName(),
+ PermanentlyPassingSecurityRule.class.getCanonicalName() )
+ .usingDataDir( folder.directory( name.getMethodName() ).getAbsolutePath() )
+ .build();
+ server.start();
+ functionalTestHelper = new FunctionalTestHelper( server );
+
+ JaxRsResponse clientResponse = gen.get()
+ .expectedStatus(403)
+ .expectedType(MediaType.APPLICATION_JSON_TYPE)
+ .get(trimTrailingSlash(functionalTestHelper.baseUri()))
+ .response();
+
+ assertEquals(403, clientResponse.getStatus());
+ }
+
+ private String trimTrailingSlash( URI uri )
+ {
+ String result = uri.toString();
+ if ( result.endsWith( "/" ) )
+ {
+ return result.substring( 0, result.length() - 1 );
+ }
+ else
+ {
+ return result;
+ }
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/security/UsersIT.java b/community/server/src/test/java/org/neo4j/server/rest/security/UsersIT.java
new file mode 100644
index 0000000000000..e9c1df25a4ee2
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/security/UsersIT.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest.security;
+
+import java.io.IOException;
+import javax.ws.rs.core.HttpHeaders;
+
+import com.sun.jersey.core.util.Base64;
+import org.codehaus.jackson.JsonNode;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.neo4j.graphdb.factory.GraphDatabaseSettings;
+import org.neo4j.kernel.impl.annotations.Documented;
+import org.neo4j.server.CommunityNeoServer;
+import org.neo4j.server.helpers.CommunityServerBuilder;
+import org.neo4j.server.rest.RESTDocsGenerator;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.string.UTF8;
+import org.neo4j.test.TestData;
+import org.neo4j.test.server.ExclusiveServerTestBase;
+import org.neo4j.test.server.HTTP;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class UsersIT extends ExclusiveServerTestBase
+{
+ public @Rule TestData gen = TestData.producedThrough( RESTDocsGenerator.PRODUCER );
+ private CommunityNeoServer server;
+
+ @Before
+ public void setUp()
+ {
+ gen.get().setSection( "dev/rest-api" );
+ }
+
+ @Test
+ @Documented( "User status\n" +
+ "\n" +
+ "Given that you know the current password, you can ask the server for the user status." )
+ public void user_status() throws JsonParseException, IOException
+ {
+ // Given
+ startServerWithConfiguredUser();
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) )
+ .get( userURL( "neo4j" ) );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ assertThat( data.get( "username" ).asText(), equalTo( "neo4j" ) );
+ assertThat( data.get( "password_change_required" ).asBoolean(), equalTo( false ) );
+ assertThat( data.get( "password_change" ).asText(), equalTo( passwordURL( "neo4j" ) ) );
+ }
+
+ @Test
+ @Documented( "User status on first access\n" +
+ "\n" +
+ "On first access, and using the default password, the user status will indicate that the users password requires changing." )
+ public void user_status_first_access() throws JsonParseException, IOException
+ {
+ // Given
+ startServer( true );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) )
+ .get( userURL( "neo4j" ) );
+
+ // Then
+ JsonNode data = JsonHelper.jsonNode( response.entity() );
+ assertThat( data.get( "username" ).asText(), equalTo( "neo4j" ) );
+ assertThat( data.get( "password_change_required" ).asBoolean(), equalTo( true ) );
+ assertThat( data.get( "password_change" ).asText(), equalTo( passwordURL( "neo4j" ) ) );
+ }
+
+ @Test
+ @Documented( "Changing the user password\n" +
+ "\n" +
+ "Given that you know the current password, you can ask the server to change a users password. You can choose any\n" +
+ "password you like, as long as it is different from the current password." )
+ public void change_password() throws JsonParseException, IOException
+ {
+ // Given
+ startServer( true );
+
+ // Document
+ RESTDocsGenerator.ResponseEntity response = gen.get()
+ .expectedStatus( 200 )
+ .withHeader( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) )
+ .payload( quotedJson( "{'password':'secret'}" ) )
+ .post( server.baseUri().resolve( "/user/neo4j/password" ).toString() );
+
+ // Then the new password should work
+ assertEquals( 200, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "secret" ) ).GET( dataURL() ).status() );
+
+ // Then the old password should not be invalid
+ assertEquals( 401, HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ).POST( dataURL() ).status() );
+ }
+
+ @Test
+ public void cantChangeToCurrentPassword() throws Exception
+ {
+ // Given
+ startServer( true );
+
+ // When
+ HTTP.Response res = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ).POST(
+ server.baseUri().resolve( "/user/neo4j/password" ).toString(),
+ HTTP.RawPayload.quotedJson( "{'password':'neo4j'}" ) );
+
+ // Then
+ assertThat( res.status(), equalTo( 422 ) );
+ }
+
+ @After
+ public void cleanup()
+ {
+ if(server != null) {server.stop();}
+ }
+
+ public void startServer(boolean authEnabled) throws IOException
+ {
+ server = CommunityServerBuilder.server()
+ .withProperty( GraphDatabaseSettings.auth_enabled.name(), Boolean.toString( authEnabled ) )
+ .build();
+ server.start();
+ }
+
+ public void startServerWithConfiguredUser() throws IOException
+ {
+ startServer( true );
+ // Set the password
+ HTTP.Response post = HTTP.withHeaders( HttpHeaders.AUTHORIZATION, challengeResponse( "neo4j", "neo4j" ) ).POST(
+ server.baseUri().resolve( "/user/neo4j/password" ).toString(),
+ HTTP.RawPayload.quotedJson( "{'password':'secret'}" )
+ );
+ assertEquals( 200, post.status() );
+ }
+
+ private String challengeResponse( String username, String password )
+ {
+ return "Basic " + base64( username + ":" + password );
+ }
+
+ private String dataURL()
+ {
+ return server.baseUri().resolve( "db/data/" ).toString();
+ }
+
+ private String userURL( String username )
+ {
+ return server.baseUri().resolve( "user/" + username ).toString();
+ }
+
+ private String passwordURL( String username )
+ {
+ return server.baseUri().resolve( "user/" + username + "/password" ).toString();
+ }
+
+ private String base64(String value)
+ {
+ return UTF8.decode( Base64.encode( value ) );
+ }
+
+ private String quotedJson( String singleQuoted )
+ {
+ return singleQuoted.replaceAll( "'", "\"" );
+ }
+}
diff --git a/community/server/src/test/java/org/neo4j/server/rest/streaming/StreamingBatchOperationIT.java b/community/server/src/test/java/org/neo4j/server/rest/streaming/StreamingBatchOperationIT.java
new file mode 100644
index 0000000000000..6ee0334346ad7
--- /dev/null
+++ b/community/server/src/test/java/org/neo4j/server/rest/streaming/StreamingBatchOperationIT.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (c) 2002-2016 "Neo Technology,"
+ * Network Engine for Objects in Lund AB [http://neotechnology.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Neo4j is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.neo4j.server.rest.streaming;
+
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import org.json.JSONException;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import org.neo4j.graphdb.Neo4jMatchers;
+import org.neo4j.graphdb.Node;
+import org.neo4j.graphdb.Transaction;
+import org.neo4j.helpers.collection.Iterables;
+import org.neo4j.server.rest.AbstractRestFunctionalTestBase;
+import org.neo4j.server.rest.JaxRsResponse;
+import org.neo4j.server.rest.PrettyJSON;
+import org.neo4j.server.rest.RestRequest;
+import org.neo4j.server.rest.domain.JsonHelper;
+import org.neo4j.server.rest.domain.JsonParseException;
+import org.neo4j.server.rest.repr.BadInputException;
+import org.neo4j.server.rest.repr.StreamingFormat;
+import org.neo4j.test.GraphDescription.Graph;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.neo4j.graphdb.Neo4jMatchers.inTx;
+
+public class StreamingBatchOperationIT extends AbstractRestFunctionalTestBase
+{
+
+ /**
+ * By specifying an extended header attribute in the HTTP request,
+ * the server will stream the results back as soon as they are processed on the server side
+ * instead of constructing a full response when all entities are processed.
+ */
+ @SuppressWarnings( "unchecked" )
+ @Test
+ @Graph("Joe knows John")
+ public void execute_multiple_operations_in_batch_streaming() throws Exception {
+ long idJoe = data.get().get( "Joe" ).getId();
+ String jsonString = new PrettyJSON()
+ .array()
+ .object()
+ .key("method") .value("PUT")
+ .key("to") .value("/node/" + idJoe + "/properties")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(0)
+ .endObject()
+ .object()
+ .key("method") .value("GET")
+ .key("to") .value("/node/" + idJoe)
+ .key("id") .value(1)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(2)
+ .endObject()
+ .object()
+ .key("method") .value("POST")
+ .key("to") .value("/node")
+ .key("body")
+ .object()
+ .key("age").value(1)
+ .endObject()
+ .key("id") .value(3)
+ .endObject()
+ .endArray().toString();
+
+
+ String entity = gen.get()
+ .expectedType( APPLICATION_JSON_TYPE )
+ .withHeader(StreamingFormat.STREAM_HEADER,"true")
+ .payload(jsonString)
+ .expectedStatus(200)
+ .post( batchUri() ).entity();
+ List> results = JsonHelper.jsonToList(entity);
+
+ assertEquals(4, results.size());
+
+ Map putResult = results.get(0);
+ Map getResult = results.get(1);
+ Map firstPostResult = results.get(2);
+ Map secondPostResult = results.get(3);
+
+ // Ids should be ok
+ assertEquals(0, putResult.get("id"));
+ assertEquals(2, firstPostResult.get("id"));
+ assertEquals(3, secondPostResult.get("id"));
+
+ // Should contain "from"
+ assertEquals("/node/"+idJoe+"/properties", putResult.get("from"));
+ assertEquals("/node/"+idJoe, getResult.get("from"));
+ assertEquals("/node", firstPostResult.get("from"));
+ assertEquals("/node", secondPostResult.get("from"));
+
+ // Post should contain location
+ assertTrue(((String) firstPostResult.get("location")).length() > 0);
+ assertTrue(((String) secondPostResult.get("location")).length() > 0);
+
+ // Should have created by the first PUT request
+ Map body = (Map) getResult.get("body");
+ assertEquals(1, ((Map