-
Notifications
You must be signed in to change notification settings - Fork 2.3k
/
TransactionTestIT.java
414 lines (364 loc) · 18.5 KB
/
TransactionTestIT.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
/*
* Copyright (c) 2002-2017 "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 <http://www.gnu.org/licenses/>.
*/
package org.neo4j.server.rest.transactional;
import org.junit.Test;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.neo4j.kernel.api.exceptions.Status;
import org.neo4j.kernel.impl.annotations.Documented;
import org.neo4j.server.rest.AbstractRestFunctionalTestBase;
import org.neo4j.server.rest.RESTRequestGenerator.ResponseEntity;
import org.neo4j.server.rest.domain.JsonParseException;
import org.neo4j.server.rest.repr.util.RFC1123;
import org.neo4j.test.server.HTTP;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsMapContaining.hasKey;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.neo4j.helpers.collection.Iterators.iterator;
import static org.neo4j.server.rest.domain.JsonHelper.jsonToMap;
import static org.neo4j.test.server.HTTP.GET;
import static org.neo4j.test.server.HTTP.POST;
public class TransactionTestIT extends AbstractRestFunctionalTestBase
{
@Test
@Documented( "Begin a transaction\n" +
"\n" +
"You begin a new transaction by posting zero or more Cypher statements\n" +
"to the transaction endpoint. The server will respond with the result of\n" +
"your statements, as well as the location of your open transaction." )
public void begin_a_transaction() throws JsonParseException
{
// Document
ResponseEntity response = gen.get()
.expectedStatus( 201 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n {props}) RETURN n', " +
"'parameters': { 'props': { 'name': 'My Node' } } } ] }" ) )
.post( getDataUri() + "transaction" );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Map<String, Object> node = resultCell( result, 0, 0 );
assertThat( node.get( "name" ), equalTo( "My Node" ) );
}
@Test
@Documented( "Execute statements in an open transaction\n" +
"\n" +
"Given that you have an open transaction, you can make a number of requests, each of which executes additional\n" +
"statements, and keeps the transaction open by resetting the transaction timeout." )
public void execute_statements_in_an_open_transaction() throws JsonParseException
{
// Given
String location = POST( getDataUri() + "transaction" ).location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN n' } ] }" ) )
.post( location );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertThat(result, hasKey( "transaction" ));
assertNoErrors( result );
}
@Test
@Documented( "Execute statements in an open transaction in REST format for the return.\n" +
"\n" +
"Given that you have an open transaction, you can make a number of requests, each of which executes additional\n" +
"statements, and keeps the transaction open by resetting the transaction timeout. Specifying the `REST` format will\n" +
"give back full Neo4j Rest API representations of the Neo4j Nodes, Relationships and Paths, if returned." )
public void execute_statements_in_an_open_transaction_using_REST() throws JsonParseException
{
// Given
String location = POST( getDataUri() + "transaction" ).location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN n','resultDataContents':['REST'] } ] }" ) )
.post( location );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
ArrayList rest = (ArrayList) ((Map)((ArrayList)((Map)((ArrayList)result.get("results")).get(0)) .get("data")).get(0)).get("rest");
String selfUri = (String) ((Map)rest.get(0)).get("self");
assertTrue(selfUri.startsWith(getDatabaseUri()));
assertNoErrors( result );
}
@Test
@Documented( "Reset transaction timeout of an open transaction\n" +
"\n" +
"Every orphaned transaction is automatically expired after a period of inactivity. This may be prevented\n" +
"by resetting the transaction timeout.\n" +
"\n" +
"The timeout may be reset by sending a keep-alive request to the server that executes an empty list of statements.\n" +
"This request will reset the transaction timeout and return the new time at which the transaction will\n" +
"expire as an RFC1123 formatted timestamp value in the ``transaction'' section of the response." )
public void reset_transaction_timeout_of_an_open_transaction()
throws JsonParseException, ParseException, InterruptedException
{
// Given
HTTP.Response initialResponse = POST( getDataUri() + "transaction" );
String location = initialResponse.location();
long initialExpirationTime = expirationTime( jsonToMap( initialResponse.rawContent() ) );
// This generous wait time is necessary to compensate for limited resolution of RFC 1123 timestamps
// and the fact that the system clock is allowed to run "backwards" between threads
// (cf. http://stackoverflow.com/questions/2978598)
//
Thread.sleep( 3000 );
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ ] }" ) )
.post( location );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
long newExpirationTime = expirationTime( result );
assertTrue( "Expiration time was not increased", newExpirationTime > initialExpirationTime );
}
@Test
@Documented( "Commit an open transaction\n" +
"\n" +
"Given you have an open transaction, you can send a commit request. Optionally, you submit additional statements\n" +
"along with the request that will be executed before committing the transaction." )
public void commit_an_open_transaction() throws JsonParseException
{
// Given
String location = POST( getDataUri() + "transaction" ).location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN id(n)' } ] }" ) )
.post( location + "/commit" );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Integer id = resultCell( result, 0, 0 );
assertThat( GET( getNodeUri( id ) ).status(), is( 200 ) );
}
@Test
@Documented( "Begin and commit a transaction in one request\n" +
"\n" +
"If there is no need to keep a transaction open across multiple HTTP requests, you can begin a transaction,\n" +
"execute statements, and commit with just a single HTTP request." )
public void begin_and_commit_a_transaction_in_one_request() throws JsonParseException
{
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN id(n)' } ] }" ) )
.post( getDataUri() + "transaction/commit" );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Integer id = resultCell( result, 0, 0 );
assertThat( GET( getNodeUri( id ) ).status(), is( 200 ) );
}
@Test
@Documented( "Execute multiple statements\n" +
"\n" +
"You can send multiple Cypher statements in the same request.\n" +
"The response will contain the result of each statement." )
public void execute_multiple_statements() throws JsonParseException
{
// Document
ResponseEntity response = gen.get().expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN id(n)' }, "
+ "{ 'statement': 'CREATE (n {props}) RETURN n', "
+ "'parameters': { 'props': { 'name': 'My Node' } } } ] }" ) )
.post( getDataUri() + "transaction/commit" );
// Then
Map<String,Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Integer id = resultCell( result, 0, 0 );
assertThat( GET( getNodeUri( id ) ).status(), is( 200 ) );
assertThat( response.entity(), containsString( "My Node" ) );
}
@Test
@Documented( "Return results in graph format\n" +
"\n" +
"If you want to understand the graph structure of nodes and relationships returned by your query,\n" +
"you can specify the \"graph\" results data format. For example, this is useful when you want to visualise the\n" +
"graph structure. The format collates all the nodes and relationships from all columns of the result,\n" +
"and also flattens collections of nodes and relationships, including paths." )
public void return_results_in_graph_format() throws JsonParseException
{
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{'statements':[{'statement':" +
"'CREATE ( bike:Bike { weight: 10 } ) " +
"CREATE ( frontWheel:Wheel { spokes: 3 } ) " +
"CREATE ( backWheel:Wheel { spokes: 32 } ) " +
"CREATE p1 = (bike)-[:HAS { position: 1 } ]->(frontWheel) " +
"CREATE p2 = (bike)-[:HAS { position: 2 } ]->(backWheel) " +
"RETURN bike, p1, p2', " +
"'resultDataContents': ['row','graph']}] }" ) )
.post( getDataUri() + "transaction/commit" );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Map<String, List<Object>> row = graphRow( result, 0 );
assertEquals( 3, row.get( "nodes" ).size() );
assertEquals( 2, row.get( "relationships" ).size() );
}
@Test
@Documented( "Rollback an open transaction\n" +
"\n" +
"Given that you have an open transaction, you can send a rollback request. The server will rollback the\n" +
"transaction. Any further statements trying to run in this transaction will fail immediately." )
public void rollback_an_open_transaction() throws JsonParseException
{
// Given
HTTP.Response firstReq = POST( getDataUri() + "transaction",
HTTP.RawPayload.quotedJson( "{ 'statements': [ { 'statement': 'CREATE (n) RETURN id(n)' } ] }" ) );
String location = firstReq.location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.delete( location );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertNoErrors( result );
Integer id = resultCell( firstReq, 0, 0 );
assertThat( GET( getNodeUri( id ) ).status(), is( 404 ) );
}
@Test
@Documented( "Handling errors\n" +
"\n" +
"The result of any request against the transaction endpoint is streamed back to the client.\n" +
"Therefore the server does not know whether the request will be successful or not when it sends the HTTP status\n" +
"code.\n" +
"\n" +
"Because of this, all requests against the transactional endpoint will return 200 or 201 status code, regardless\n" +
"of whether statements were successfully executed. At the end of the response payload, the server includes a list\n" +
"of errors that occurred while executing statements. If this list is empty, the request completed successfully.\n" +
"\n" +
"If any errors occur while executing statements, the server will roll back the transaction.\n" +
"\n" +
"In this example, we send the server an invalid statement to demonstrate error handling.\n" +
" \n" +
"For more information on the status codes, see <<status-codes>>." )
public void handling_errors() throws JsonParseException
{
// Given
String location = POST( getDataUri() + "transaction" ).location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'This is not a valid Cypher Statement.' } ] }" ) )
.post( location + "/commit" );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertErrors( result, Status.Statement.SyntaxError );
}
@Test
@Documented( "Handling errors in an open transaction\n" +
"\n" +
"Whenever there is an error in a request the server will rollback the transaction.\n" +
"By inspecting the response for the presence/absence of the `transaction` key you can tell if the " +
"transaction is still open" )
public void errors_in_open_transaction() throws JsonParseException
{
// Given
String location = POST( getDataUri() + "transaction" ).location();
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson( "{ 'statements': [ { 'statement': 'This is not a valid Cypher Statement.' } ] }" ) )
.post( location );
// Then
Map<String, Object> result = jsonToMap( response.entity() );
assertThat(result, not(hasKey( "transaction" )));
}
@Test
@Documented( "Include query statistics\n" +
"\n" +
"By setting `includeStats` to `true` for a statement, query statistics will be returned for it." )
public void include_query_statistics() throws JsonParseException
{
// Document
ResponseEntity response = gen.get()
.expectedStatus( 200 )
.payload( quotedJson(
"{ 'statements': [ { 'statement': 'CREATE (n) RETURN id(n)', 'includeStats': true } ] }" ) )
.post( getDataUri() + "transaction/commit" );
// Then
Map<String,Object> entity = jsonToMap( response.entity() );
assertNoErrors( entity );
Map<String,Object> firstResult = ((List<Map<String,Object>>) entity.get( "results" )).get( 0 );
assertThat( firstResult, hasKey( "stats" ) );
Map<String,Object> stats = (Map<String,Object>) firstResult.get( "stats" );
assertThat( stats.get( "nodes_created" ), equalTo( 1 ) );
}
private void assertNoErrors( Map<String, Object> response )
{
assertErrors( response );
}
private void assertErrors( Map<String, Object> response, Status... expectedErrors )
{
@SuppressWarnings( "unchecked" )
Iterator<Map<String, Object>> errors = ((List<Map<String, Object>>) response.get( "errors" )).iterator();
Iterator<Status> expected = iterator( expectedErrors );
while ( expected.hasNext() )
{
assertTrue( errors.hasNext() );
assertThat( errors.next().get( "code" ), equalTo( expected.next().code().serialize() ) );
}
if ( errors.hasNext() )
{
Map<String, Object> error = errors.next();
fail( "Expected no more errors, but got " + error.get( "code" ) + " - '" + error.get( "message" ) + "'." );
}
}
private <T> T resultCell( HTTP.Response response, int row, int column )
{
return resultCell( response.<Map<String, Object>>content(), row, column );
}
@SuppressWarnings( "unchecked" )
private <T> T resultCell( Map<String, Object> response, int row, int column )
{
Map<String, Object> result = ((List<Map<String, Object>>) response.get( "results" )).get( 0 );
List<Map<String,List>> data = (List<Map<String,List>>) result.get( "data" );
return (T) data.get( row ).get( "row" ).get( column );
}
@SuppressWarnings( "unchecked" )
private Map<String, List<Object>> graphRow( Map<String, Object> response, int row )
{
Map<String, Object> result = ((List<Map<String, Object>>) response.get( "results" )).get( 0 );
List<Map<String,List>> data = (List<Map<String,List>>) result.get( "data" );
return (Map<String,List<Object>>) data.get( row ).get( "graph" );
}
private String quotedJson( String singleQuoted )
{
return singleQuoted.replaceAll( "'", "\"" );
}
private long expirationTime( Map<String, Object> entity ) throws ParseException
{
String timestampString = (String) ( (Map<?, ?>) entity.get( "transaction" ) ).get( "expires" );
return RFC1123.parseTimestamp( timestampString ).getTime();
}
}