boltless 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +8 -0
- data/Guardfile +44 -0
- data/Makefile +138 -0
- data/Rakefile +26 -0
- data/docker-compose.yml +19 -0
- data/lib/boltless/configuration.rb +69 -0
- data/lib/boltless/errors/invalid_json_error.rb +9 -0
- data/lib/boltless/errors/request_error.rb +24 -0
- data/lib/boltless/errors/response_error.rb +30 -0
- data/lib/boltless/errors/transaction_begin_error.rb +9 -0
- data/lib/boltless/errors/transaction_in_bad_state_error.rb +11 -0
- data/lib/boltless/errors/transaction_not_found_error.rb +11 -0
- data/lib/boltless/errors/transaction_rollback_error.rb +26 -0
- data/lib/boltless/extensions/configuration_handling.rb +37 -0
- data/lib/boltless/extensions/connection_pool.rb +127 -0
- data/lib/boltless/extensions/operations.rb +175 -0
- data/lib/boltless/extensions/transactions.rb +301 -0
- data/lib/boltless/extensions/utilities.rb +187 -0
- data/lib/boltless/request.rb +386 -0
- data/lib/boltless/result.rb +98 -0
- data/lib/boltless/result_row.rb +90 -0
- data/lib/boltless/statement_collector.rb +40 -0
- data/lib/boltless/transaction.rb +234 -0
- data/lib/boltless/version.rb +23 -0
- data/lib/boltless.rb +36 -0
- data/spec/benchmark/transfer.rb +57 -0
- data/spec/boltless/extensions/configuration_handling_spec.rb +39 -0
- data/spec/boltless/extensions/connection_pool_spec.rb +131 -0
- data/spec/boltless/extensions/operations_spec.rb +189 -0
- data/spec/boltless/extensions/transactions_spec.rb +418 -0
- data/spec/boltless/extensions/utilities_spec.rb +546 -0
- data/spec/boltless/request_spec.rb +946 -0
- data/spec/boltless/result_row_spec.rb +161 -0
- data/spec/boltless/result_spec.rb +127 -0
- data/spec/boltless/statement_collector_spec.rb +45 -0
- data/spec/boltless/transaction_spec.rb +601 -0
- data/spec/boltless_spec.rb +11 -0
- data/spec/fixtures/files/raw_result.yml +21 -0
- data/spec/fixtures/files/raw_result_with_graph_result.yml +48 -0
- data/spec/fixtures/files/raw_result_with_meta.yml +11 -0
- data/spec/fixtures/files/raw_result_with_stats.yml +26 -0
- data/spec/spec_helper.rb +89 -0
- metadata +384 -0
@@ -0,0 +1,946 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
# rubocop:disable RSpec/NestedGroups because nesting makes sense here
|
6
|
+
RSpec.describe Boltless::Request do
|
7
|
+
let(:new_instance) { ->(**args) { described_class.new(connection, **args) } }
|
8
|
+
let(:instance) { new_instance.call }
|
9
|
+
let(:connection) { Boltless.connection_pool.checkout }
|
10
|
+
let(:catch_exception) do
|
11
|
+
proc do |&block|
|
12
|
+
block.call
|
13
|
+
rescue StandardError => e
|
14
|
+
e
|
15
|
+
end
|
16
|
+
end
|
17
|
+
let(:tx_id) { nil }
|
18
|
+
let(:response) do
|
19
|
+
HTTP::Response.new(
|
20
|
+
status: http_status_code,
|
21
|
+
version: '1.1',
|
22
|
+
headers: headers,
|
23
|
+
body: body,
|
24
|
+
request: nil
|
25
|
+
)
|
26
|
+
end
|
27
|
+
let(:body) { '' }
|
28
|
+
let(:headers) { {} }
|
29
|
+
let(:http_status_code) { 200 }
|
30
|
+
let(:statements) do
|
31
|
+
[
|
32
|
+
{
|
33
|
+
statement: 'RETURN 1',
|
34
|
+
parameters: {}
|
35
|
+
},
|
36
|
+
{
|
37
|
+
statement: 'RETURN $arg',
|
38
|
+
parameters: { arg: 2 }
|
39
|
+
}
|
40
|
+
]
|
41
|
+
end
|
42
|
+
let(:logger) { Logger.new(log_dev, level: :debug) }
|
43
|
+
let(:log_dev) { StringIO.new }
|
44
|
+
let(:log) { log_dev.string.uncolorize }
|
45
|
+
|
46
|
+
before { Boltless.configuration.logger = logger }
|
47
|
+
|
48
|
+
describe 'full workflow' do
|
49
|
+
let(:action) do
|
50
|
+
id = instance.begin_transaction
|
51
|
+
instance.run_query(id, *statements).tap do
|
52
|
+
instance.rollback_transaction(id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
let(:statements) do
|
56
|
+
[
|
57
|
+
# Create a new user named Klaus
|
58
|
+
{
|
59
|
+
statement: 'CREATE (n:User { name: $name })',
|
60
|
+
parameters: { name: 'Klaus' }
|
61
|
+
},
|
62
|
+
# Create a new user named Bernd
|
63
|
+
{
|
64
|
+
statement: 'CREATE (n:User { name: $name })',
|
65
|
+
parameters: { name: 'Bernd' }
|
66
|
+
},
|
67
|
+
# Create a relationship between Klaus and Bernd
|
68
|
+
# to signalize they are friends
|
69
|
+
{
|
70
|
+
statement: 'MATCH (a:User), (b:User) ' \
|
71
|
+
'WHERE a.name = $name_a AND b.name = $name_b ' \
|
72
|
+
'CREATE (a)-[:FRIEND_OF]->(b)',
|
73
|
+
parameters: { name_a: 'Klaus', name_b: 'Bernd' }
|
74
|
+
},
|
75
|
+
# Then ask for all friends of Klaus
|
76
|
+
{
|
77
|
+
statement: 'MATCH (l:User { name: $name })-[:FRIEND_OF]->(r:User) ' \
|
78
|
+
'RETURN r.name AS name',
|
79
|
+
parameters: { name: 'Klaus' }
|
80
|
+
}
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'returns 4 results' do
|
85
|
+
expect(action.count).to be_eql(4)
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns the expected results (first 3)' do
|
89
|
+
expect(action[0..2].map(&:count)).to all(be_eql(0))
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'returns the expected results (last)' do
|
93
|
+
expect(action.last.value).to be_eql('Bernd')
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'does not write the data actually' do
|
97
|
+
count = instance.one_shot_transaction(
|
98
|
+
{ statement: 'MATCH (n:User) RETURN count(n)' }
|
99
|
+
).first.value
|
100
|
+
expect(count).to be_eql(0)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '.statement_payloads' do
|
105
|
+
let(:action) { ->(*args) { described_class.statement_payloads(*args) } }
|
106
|
+
|
107
|
+
it 'returns an Array' do
|
108
|
+
expect(action.call).to be_a(Array)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'returns an array with the converted statements' do
|
112
|
+
expect(action.call(['a', { a: true }], ['b', {}]).count).to be_eql(2)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'returns an array of converted statement Hashes' do
|
116
|
+
expect(action.call(['a', { a: true }], ['b', {}])).to all(be_a(Hash))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe '.statement_payload' do
|
121
|
+
let(:action) do
|
122
|
+
->(cypher, **args) { described_class.statement_payload(cypher, **args) }
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'without parameters' do
|
126
|
+
let(:action) { super().call('cypher') }
|
127
|
+
|
128
|
+
it 'returns a Hash instance' do
|
129
|
+
expect(action).to be_a(Hash)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'returns a correct statement structure' do
|
133
|
+
expect(action).to match(statement: 'cypher', parameters: {})
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'with user parameters only' do
|
138
|
+
let(:action) { super().call('cypher', a: { b: true }) }
|
139
|
+
|
140
|
+
it 'returns a Hash instance' do
|
141
|
+
expect(action).to be_a(Hash)
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'returns a correct statement structure' do
|
145
|
+
expect(action).to \
|
146
|
+
match(statement: 'cypher', parameters: { a: { b: true } })
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
context 'with statistics parameter' do
|
151
|
+
let(:action) { super().call('cypher', with_stats: true, a: { b: true }) }
|
152
|
+
|
153
|
+
it 'returns a Hash instance' do
|
154
|
+
expect(action).to be_a(Hash)
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'returns a correct statement structure' do
|
158
|
+
expect(action).to \
|
159
|
+
match(statement: 'cypher',
|
160
|
+
includeStats: true,
|
161
|
+
parameters: { a: { b: true } })
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'with graph output parameter' do
|
166
|
+
let(:action) { super().call('cypher', result_as_graph: true, a: 1) }
|
167
|
+
|
168
|
+
it 'returns a Hash instance' do
|
169
|
+
expect(action).to be_a(Hash)
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'returns a correct statement structure' do
|
173
|
+
expect(action).to \
|
174
|
+
match(statement: 'cypher',
|
175
|
+
resultDataContents: %w[row graph],
|
176
|
+
parameters: { a: 1 })
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
describe '#one_shot_transaction' do
|
182
|
+
let(:action) { ->(*args) { instance.one_shot_transaction(*args) } }
|
183
|
+
let(:body) { '{}' }
|
184
|
+
let(:req_body) { { statements: statements }.to_json }
|
185
|
+
|
186
|
+
before do
|
187
|
+
allow(connection).to receive(:headers).and_return(connection)
|
188
|
+
allow(connection).to receive(:post).and_return(response)
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'without statements' do
|
192
|
+
it 'raises an ArgumentError' do
|
193
|
+
expect { action.call }.to \
|
194
|
+
raise_error(ArgumentError, /No statements given/)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'wraps the user block inside a #handle_transaction call' do
|
199
|
+
expect(instance).to receive(:handle_transaction).with(tx_id: 'commit')
|
200
|
+
action.call(*statements)
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'sends a HTTP POST request' do
|
204
|
+
expect(connection).to \
|
205
|
+
receive(:post).with('/db/neo4j/tx/commit', body: req_body)
|
206
|
+
.and_return(response)
|
207
|
+
action.call(*statements)
|
208
|
+
end
|
209
|
+
|
210
|
+
describe 'logging' do
|
211
|
+
before do
|
212
|
+
Boltless.configuration.query_log_enabled = true
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'logs the one-shot transaction' do
|
216
|
+
action.call(statements.first)
|
217
|
+
expect(log).to match(/\[tx:write:one-shot\] \(.*ms\) RETURN 1/)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe '#begin_transaction' do
|
223
|
+
let(:action) { ->(*args) { instance.begin_transaction(*args) } }
|
224
|
+
let(:body) { '{}' }
|
225
|
+
|
226
|
+
before do
|
227
|
+
allow(connection).to receive(:headers).and_return(connection)
|
228
|
+
allow(connection).to receive(:post).and_return(response)
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'sets the correct access mode header' do
|
232
|
+
expect(connection).to \
|
233
|
+
receive(:headers).with('Access-Mode' => 'WRITE').and_call_original
|
234
|
+
action.call
|
235
|
+
end
|
236
|
+
|
237
|
+
context 'with an unsuccessful response' do
|
238
|
+
let(:http_status_code) { 500 }
|
239
|
+
let(:body) { 'Something went wrong' }
|
240
|
+
|
241
|
+
it 'raises an Boltless::Errors::TransactionBeginError' do
|
242
|
+
expect { action.call }.to \
|
243
|
+
raise_error(Boltless::Errors::TransactionBeginError,
|
244
|
+
/Something went wrong/)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'with an successful response without Location header' do
|
249
|
+
let(:body) { '{"some":"thing"}' }
|
250
|
+
|
251
|
+
it 'raises an Boltless::Errors::TransactionBegin' do
|
252
|
+
expect { action.call }.to \
|
253
|
+
raise_error(Boltless::Errors::TransactionBeginError,
|
254
|
+
/{"some":"thing"}/)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context 'with an successful response with Location header' do
|
259
|
+
let(:headers) { { 'location' => 'http://neo4j:7474/db/neo4j/tx/3894' } }
|
260
|
+
|
261
|
+
it 'returns the correct transaction identifier' do
|
262
|
+
expect(action.call).to be_eql(3894)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
describe 'logging' do
|
267
|
+
let(:headers) { { 'location' => 'http://neo4j:7474/db/neo4j/tx/3894' } }
|
268
|
+
|
269
|
+
before { Boltless.configuration.query_log_enabled = :debug }
|
270
|
+
|
271
|
+
it 'logs the start of the transaction' do
|
272
|
+
action.call
|
273
|
+
expect(log).to match(/\[tx:write:3894 rq:1\] \(.*ms\) BEGIN/)
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'logs the start of the transaction (debug)' do
|
277
|
+
action.call
|
278
|
+
expect(log).to match(/\[tx:write:tbd rq:1\] BEGIN/)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe '#run_query' do
|
284
|
+
let(:action) do
|
285
|
+
proc do |*args|
|
286
|
+
instance.run_query(8134, *args)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
let(:body) { '{}' }
|
290
|
+
|
291
|
+
it 'wraps the user block inside a #handle_transaction call' do
|
292
|
+
expect(instance).to receive(:handle_transaction).with(tx_id: 8134)
|
293
|
+
action.call(*statements)
|
294
|
+
end
|
295
|
+
|
296
|
+
context 'without statements' do
|
297
|
+
it 'raises an ArgumentError' do
|
298
|
+
expect { action.call }.to \
|
299
|
+
raise_error(ArgumentError, /No statements given/)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
context 'with statements' do
|
304
|
+
let(:req_body) { { statements: statements }.to_json }
|
305
|
+
|
306
|
+
it 'sends a HTTP POST request' do
|
307
|
+
expect(connection).to \
|
308
|
+
receive(:post).with('/db/neo4j/tx/8134', body: req_body)
|
309
|
+
.and_return(response)
|
310
|
+
action.call(*statements)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe 'logging' do
|
315
|
+
before do
|
316
|
+
allow(connection).to receive(:post).and_return(response)
|
317
|
+
Boltless.configuration.query_log_enabled = true
|
318
|
+
end
|
319
|
+
|
320
|
+
it 'logs the query within the transaction' do
|
321
|
+
action.call(statements.first)
|
322
|
+
expect(log).to match(/\[tx:write:8134 rq:1\] \(.*ms\) RETURN 1/)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
describe '#commit_transaction' do
|
328
|
+
let(:action) do
|
329
|
+
proc do |*args|
|
330
|
+
instance.commit_transaction(8134, *args)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
let(:body) { '{}' }
|
334
|
+
|
335
|
+
it 'wraps the user block inside a #handle_transaction call' do
|
336
|
+
expect(instance).to receive(:handle_transaction).with(tx_id: 8134)
|
337
|
+
action.call
|
338
|
+
end
|
339
|
+
|
340
|
+
context 'without finalizing statements' do
|
341
|
+
it 'sends a HTTP POST request' do
|
342
|
+
opts = {}
|
343
|
+
expect(connection).to \
|
344
|
+
receive(:post).with('/db/neo4j/tx/8134/commit', **opts)
|
345
|
+
.and_return(response)
|
346
|
+
action.call
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context 'with finalizing statements' do
|
351
|
+
let(:statements) do
|
352
|
+
[
|
353
|
+
{
|
354
|
+
statement: 'RETURN 1',
|
355
|
+
parameters: {}
|
356
|
+
},
|
357
|
+
{
|
358
|
+
statement: 'RETURN $arg',
|
359
|
+
parameters: { arg: 2 }
|
360
|
+
}
|
361
|
+
]
|
362
|
+
end
|
363
|
+
let(:req_body) { { statements: statements }.to_json }
|
364
|
+
|
365
|
+
it 'sends a HTTP POST request' do
|
366
|
+
expect(connection).to \
|
367
|
+
receive(:post).with('/db/neo4j/tx/8134/commit', body: req_body)
|
368
|
+
.and_return(response)
|
369
|
+
action.call(*statements)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
describe 'logging' do
|
374
|
+
before do
|
375
|
+
Boltless.configuration.query_log_enabled = true
|
376
|
+
allow(connection).to receive(:post).and_return(response)
|
377
|
+
end
|
378
|
+
|
379
|
+
it 'logs the commit' do
|
380
|
+
action.call
|
381
|
+
expect(log).to match(/\[tx:write:8134 rq:1\] \(.*ms\) COMMIT/)
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
describe '#rollback_transaction' do
|
387
|
+
let(:action) do
|
388
|
+
proc do
|
389
|
+
instance.rollback_transaction(8134)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
let(:body) { '{}' }
|
393
|
+
|
394
|
+
it 'wraps the user block inside a #handle_transaction call' do
|
395
|
+
expect(instance).to receive(:handle_transaction).with(tx_id: 8134)
|
396
|
+
action.call
|
397
|
+
end
|
398
|
+
|
399
|
+
it 'sends a HTTP DELETE request' do
|
400
|
+
expect(connection).to \
|
401
|
+
receive(:delete).with('/db/neo4j/tx/8134').and_return(response)
|
402
|
+
action.call
|
403
|
+
end
|
404
|
+
|
405
|
+
describe 'logging' do
|
406
|
+
before do
|
407
|
+
Boltless.configuration.query_log_enabled = true
|
408
|
+
allow(connection).to receive(:delete).and_return(response)
|
409
|
+
end
|
410
|
+
|
411
|
+
it 'logs the rollback' do
|
412
|
+
action.call
|
413
|
+
expect(log).to match(/\[tx:write:8134 rq:1\] \(.*ms\) ROLLBACK/)
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
describe '#handle_transaction' do
|
419
|
+
let(:action) do
|
420
|
+
proc do |&block|
|
421
|
+
instance.handle_transaction(tx_id: tx_id) do |*args|
|
422
|
+
block&.call(*args)
|
423
|
+
response
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
let(:tx_id) { 8134 }
|
428
|
+
let(:body) { '{}' }
|
429
|
+
|
430
|
+
it 'yields the given block' do
|
431
|
+
expect { |control| action.call(&control) }.to yield_control
|
432
|
+
end
|
433
|
+
|
434
|
+
it 'passes down the transaction path to the given block' do
|
435
|
+
expect { |control| action.call(&control) }.to \
|
436
|
+
yield_with_args('/db/neo4j/tx/8134')
|
437
|
+
end
|
438
|
+
|
439
|
+
context 'with a 404 HTTP status code' do
|
440
|
+
let(:http_status_code) { 404 }
|
441
|
+
let(:body) { 'Not found' }
|
442
|
+
|
443
|
+
it 'raises a Boltless::Errors::TransactionNotFoundError' do
|
444
|
+
expect { action.call }.to \
|
445
|
+
raise_error(Boltless::Errors::TransactionNotFoundError,
|
446
|
+
/Not found/)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
context 'with an unsuccessful status code' do
|
451
|
+
let(:http_status_code) { 500 }
|
452
|
+
let(:body) { 'Unknown error happend' }
|
453
|
+
|
454
|
+
it 'raises a Boltless::Errors::TransactionRollbackError' do
|
455
|
+
expect { action.call }.to \
|
456
|
+
raise_error(Boltless::Errors::TransactionRollbackError,
|
457
|
+
/Unknown error happend/)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
context 'with a successful status code' do
|
462
|
+
let(:http_status_code) { 200 }
|
463
|
+
let(:body) { '{}' }
|
464
|
+
|
465
|
+
it 'calls the #handle_response_body method' do
|
466
|
+
expect(instance).to \
|
467
|
+
receive(:handle_response_body).with(response, tx_id: 8134)
|
468
|
+
action.call
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
describe '#handle_response_body' do
|
474
|
+
let(:action) { instance.handle_response_body(response, tx_id: tx_id) }
|
475
|
+
|
476
|
+
context 'with invalid JSON response' do
|
477
|
+
let(:body) { '<html>Service unavailable</html>' }
|
478
|
+
|
479
|
+
it 'raises a Boltless::Errors::InvalidJsonError' do
|
480
|
+
expect { action }.to \
|
481
|
+
raise_error(Boltless::Errors::InvalidJsonError,
|
482
|
+
/JSON document has an improper structure/i)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
context 'with an empty response' do
|
487
|
+
let(:body) { '{}' }
|
488
|
+
|
489
|
+
it 'does not raise errors' do
|
490
|
+
expect { action }.not_to raise_error
|
491
|
+
end
|
492
|
+
|
493
|
+
it 'returns an empty array' do
|
494
|
+
expect(action).to match_array([])
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
context 'with results' do
|
499
|
+
let(:body) { { results: raw_results }.to_json }
|
500
|
+
let(:raw_results) do
|
501
|
+
[
|
502
|
+
{
|
503
|
+
columns: %w[a b],
|
504
|
+
data: [
|
505
|
+
{
|
506
|
+
row: [1, 2],
|
507
|
+
meta: [nil, nil]
|
508
|
+
}
|
509
|
+
]
|
510
|
+
}
|
511
|
+
]
|
512
|
+
end
|
513
|
+
|
514
|
+
context 'with raw results' do
|
515
|
+
let(:instance) { new_instance[raw_results: true] }
|
516
|
+
|
517
|
+
it 'does not raise errors' do
|
518
|
+
expect { action }.not_to raise_error
|
519
|
+
end
|
520
|
+
|
521
|
+
it 'returns the untouched response' do
|
522
|
+
expect(action).to match_array(raw_results)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
context 'with restructured results' do
|
527
|
+
let(:instance) { new_instance[raw_results: false] }
|
528
|
+
|
529
|
+
it 'does not raise errors' do
|
530
|
+
expect { action }.not_to raise_error
|
531
|
+
end
|
532
|
+
|
533
|
+
it 'returns the restructured response' do
|
534
|
+
expect(action).to all(be_a(Boltless::Result))
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
context 'with a single error' do
|
540
|
+
let(:tx_id) { 777 }
|
541
|
+
let(:body) do
|
542
|
+
{
|
543
|
+
errors: [
|
544
|
+
{
|
545
|
+
code: 'com.neo4j.some.thing',
|
546
|
+
message: 'NullPointerException'
|
547
|
+
}
|
548
|
+
]
|
549
|
+
}.to_json
|
550
|
+
end
|
551
|
+
let(:error_message) do
|
552
|
+
'Transaction \(777\) rolled back due to errors \(1\)' \
|
553
|
+
'.*NullPointerException \(com.neo4j.some.thing\)'
|
554
|
+
end
|
555
|
+
|
556
|
+
it 'raises a Boltless::Errors::TransactionRollbackError' do
|
557
|
+
expect { action }.to \
|
558
|
+
raise_error(Boltless::Errors::TransactionRollbackError,
|
559
|
+
/#{error_message}/m)
|
560
|
+
end
|
561
|
+
|
562
|
+
it 'allows to access the wrapped error' do
|
563
|
+
expect(catch_exception.call { action }.errors.first).to \
|
564
|
+
be_a(Boltless::Errors::ResponseError)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
context 'with a multiple errors' do
|
569
|
+
let(:tx_id) { 666 }
|
570
|
+
let(:body) do
|
571
|
+
{
|
572
|
+
errors: [
|
573
|
+
{
|
574
|
+
code: 'com.neo4j.some.thing1',
|
575
|
+
message: 'StringIndexOutOfBoundsException'
|
576
|
+
},
|
577
|
+
{
|
578
|
+
code: 'com.neo4j.some.thing2',
|
579
|
+
message: 'NullPointerException'
|
580
|
+
}
|
581
|
+
]
|
582
|
+
}.to_json
|
583
|
+
end
|
584
|
+
let(:error_message) do
|
585
|
+
'Transaction \(666\) rolled back due to errors \(2\)' \
|
586
|
+
'.*StringIndexOutOfBoundsException \(com.neo4j.some.thing1\)' \
|
587
|
+
'.*NullPointerException \(com.neo4j.some.thing2\)'
|
588
|
+
end
|
589
|
+
|
590
|
+
it 'raises a Boltless::Errors::TransactionRollbackError' do
|
591
|
+
expect { action }.to \
|
592
|
+
raise_error(Boltless::Errors::TransactionRollbackError,
|
593
|
+
/#{error_message}/m)
|
594
|
+
end
|
595
|
+
|
596
|
+
it 'allows to access the wrapped errors' do
|
597
|
+
expect(catch_exception.call { action }.errors).to \
|
598
|
+
all(be_a(Boltless::Errors::ResponseError))
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
describe '#serialize_body' do
|
604
|
+
let(:action) { ->(obj) { instance.serialize_body(obj) } }
|
605
|
+
|
606
|
+
context 'with an Array' do
|
607
|
+
it 'returns the expected JSON representation' do
|
608
|
+
expect(action[[1, [2]]]).to be_eql('[1,[2]]')
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
context 'with a Hash' do
|
613
|
+
it 'returns the expected JSON representation' do
|
614
|
+
expect(action[{ a: { b: true } }]).to be_eql('{"a":{"b":true}}')
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
context 'with a String' do
|
619
|
+
it 'returns the expected JSON representation' do
|
620
|
+
expect(action['test']).to be_eql('"test"')
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
describe '#handle_transport_errors' do
|
626
|
+
let(:action) do
|
627
|
+
instance.handle_transport_errors do
|
628
|
+
raise HTTP::Error, 'Something went wrong'
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
it 're-raises any HTTP::Error as Boltless::Errors::RequestError' do
|
633
|
+
expect { action }.to raise_error(Boltless::Errors::RequestError,
|
634
|
+
/Something went wrong/)
|
635
|
+
end
|
636
|
+
end
|
637
|
+
|
638
|
+
describe '#log_query' do
|
639
|
+
let(:action) do
|
640
|
+
proc do |&block|
|
641
|
+
block ||= user_block
|
642
|
+
instance.log_query(tx_id, *statements, &block)
|
643
|
+
end
|
644
|
+
end
|
645
|
+
let(:tx_id) { 8934 }
|
646
|
+
let(:statements) do
|
647
|
+
[{ statement: 'RETURN date()' }, { statement: 'RETURN 1' }]
|
648
|
+
end
|
649
|
+
let(:user_block) { -> { 923 } }
|
650
|
+
|
651
|
+
before do
|
652
|
+
Boltless.configuration.query_log_enabled = conf_value
|
653
|
+
end
|
654
|
+
|
655
|
+
context 'when query logging is disabled (false)' do
|
656
|
+
let(:conf_value) { false }
|
657
|
+
|
658
|
+
it 'yields the user block' do
|
659
|
+
expect { |control| action[&control] }.to yield_control
|
660
|
+
end
|
661
|
+
|
662
|
+
it 'returns the result of the user block' do
|
663
|
+
expect(action.call).to be_eql(923)
|
664
|
+
end
|
665
|
+
|
666
|
+
it 'does not change the request counter' do
|
667
|
+
expect { action.call }.not_to \
|
668
|
+
change { instance.instance_variable_get(:@requests_done) }.from(0)
|
669
|
+
end
|
670
|
+
|
671
|
+
it 'does not call the logger' do
|
672
|
+
expect(Boltless.logger).not_to receive(:debug)
|
673
|
+
action.call
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
context 'when query logging is enabled (true)' do
|
678
|
+
let(:conf_value) { true }
|
679
|
+
|
680
|
+
it 'yields the user block' do
|
681
|
+
expect { |control| action[&control] }.to yield_control
|
682
|
+
end
|
683
|
+
|
684
|
+
it 'returns the result of the user block' do
|
685
|
+
expect(action.call).to be_eql(923)
|
686
|
+
end
|
687
|
+
|
688
|
+
it 'change the request counter' do
|
689
|
+
expect { action.call }.to \
|
690
|
+
change { instance.instance_variable_get(:@requests_done) }
|
691
|
+
.from(0).to(1)
|
692
|
+
end
|
693
|
+
|
694
|
+
it 'calls the logger' do
|
695
|
+
expect(Boltless.logger).to receive(:debug).once
|
696
|
+
action.call
|
697
|
+
end
|
698
|
+
|
699
|
+
it 'calls the logger without arguments' do
|
700
|
+
expect(Boltless.logger).to receive(:debug).with(no_args)
|
701
|
+
action.call
|
702
|
+
end
|
703
|
+
|
704
|
+
it 'calls the logger with a block' do
|
705
|
+
allow(Boltless.logger).to receive(:debug) do |&block|
|
706
|
+
expect(block).to be_a(Proc)
|
707
|
+
end
|
708
|
+
action.call
|
709
|
+
end
|
710
|
+
|
711
|
+
it 'calls the #generate_log_str with the transaction identifier' do
|
712
|
+
expect(instance).to \
|
713
|
+
receive(:generate_log_str).with(8934, anything, anything, anything)
|
714
|
+
action.call
|
715
|
+
end
|
716
|
+
|
717
|
+
it 'calls the #generate_log_str with the mesaured duration' do
|
718
|
+
expect(instance).to \
|
719
|
+
receive(:generate_log_str).with(anything, Float, anything, anything)
|
720
|
+
action.call
|
721
|
+
end
|
722
|
+
|
723
|
+
it 'calls the #generate_log_str with the statements' do
|
724
|
+
expect(instance).to \
|
725
|
+
receive(:generate_log_str).with(anything, anything, *statements)
|
726
|
+
action.call
|
727
|
+
end
|
728
|
+
|
729
|
+
context 'with :begin as transaction identifier' do
|
730
|
+
let(:tx_id) { :begin }
|
731
|
+
let(:statements) { [{ statement: 'RETURN date()' }] }
|
732
|
+
|
733
|
+
it 'calls the #generate_log_str with block result ' \
|
734
|
+
'as transaction identifier' do
|
735
|
+
expect(instance).to \
|
736
|
+
receive(:generate_log_str).with(923, anything, anything)
|
737
|
+
action.call
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
context 'when query logging is enabled (:debug)' do
|
743
|
+
let(:conf_value) { :debug }
|
744
|
+
|
745
|
+
it 'yields the user block' do
|
746
|
+
expect { |control| action[&control] }.to yield_control
|
747
|
+
end
|
748
|
+
|
749
|
+
it 'returns the result of the user block' do
|
750
|
+
expect(action.call).to be_eql(923)
|
751
|
+
end
|
752
|
+
|
753
|
+
it 'calls the logger twice' do
|
754
|
+
expect(Boltless.logger).to receive(:debug).twice
|
755
|
+
action.call
|
756
|
+
end
|
757
|
+
|
758
|
+
describe 'the extra debug logging call' do
|
759
|
+
# NOTE: We prepare to run only the "half" of the method, by raising
|
760
|
+
# within the user block. This results in a single +#generate_log_str+
|
761
|
+
# call, the one we want to inspect. Other we would have to distinguish
|
762
|
+
# them with more complex logic.
|
763
|
+
let(:action) do
|
764
|
+
saction = super()
|
765
|
+
-> { suppress(StandardError) { saction.call } }
|
766
|
+
end
|
767
|
+
let(:user_block) { -> { raise } }
|
768
|
+
|
769
|
+
it 'calls the logger' do
|
770
|
+
expect(Boltless.logger).to receive(:debug).once
|
771
|
+
action.call
|
772
|
+
end
|
773
|
+
|
774
|
+
it 'calls the logger without arguments' do
|
775
|
+
expect(Boltless.logger).to receive(:debug).with(no_args)
|
776
|
+
action.call
|
777
|
+
end
|
778
|
+
|
779
|
+
it 'calls the logger with a block' do
|
780
|
+
allow(Boltless.logger).to receive(:debug) do |&block|
|
781
|
+
expect(block).to be_a(Proc)
|
782
|
+
end
|
783
|
+
action.call
|
784
|
+
end
|
785
|
+
|
786
|
+
it 'calls the #generate_log_str with the transaction identifier' do
|
787
|
+
expect(instance).to \
|
788
|
+
receive(:generate_log_str).with(8934, anything, anything, anything)
|
789
|
+
action.call
|
790
|
+
end
|
791
|
+
|
792
|
+
it 'calls the #generate_log_str without a duration' do
|
793
|
+
expect(instance).to \
|
794
|
+
receive(:generate_log_str).with(anything, nil, anything, anything)
|
795
|
+
action.call
|
796
|
+
end
|
797
|
+
|
798
|
+
it 'calls the #generate_log_str with the statements' do
|
799
|
+
expect(instance).to \
|
800
|
+
receive(:generate_log_str).with(anything, anything, *statements)
|
801
|
+
action.call
|
802
|
+
end
|
803
|
+
|
804
|
+
context 'with :begin as transaction identifier' do
|
805
|
+
let(:tx_id) { :begin }
|
806
|
+
let(:statements) { [{ statement: 'RETURN date()' }] }
|
807
|
+
|
808
|
+
it 'calls the #generate_log_str with "tbd" ' \
|
809
|
+
'as transaction identifier' do
|
810
|
+
expect(instance).to \
|
811
|
+
receive(:generate_log_str).with('tbd', anything, anything)
|
812
|
+
action.call
|
813
|
+
end
|
814
|
+
end
|
815
|
+
end
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
describe '#generate_log_str' do
|
820
|
+
let(:action) do
|
821
|
+
instance.generate_log_str(tx_id, duration, *statements).uncolorize
|
822
|
+
end
|
823
|
+
let(:tx_id) { 594 }
|
824
|
+
let(:duration) { 9.5 }
|
825
|
+
let(:statements) do
|
826
|
+
[
|
827
|
+
{
|
828
|
+
statement: 'RETURN $arg',
|
829
|
+
parameters: { arg: 2 }
|
830
|
+
}
|
831
|
+
]
|
832
|
+
end
|
833
|
+
|
834
|
+
it 'follows the log pattern' do
|
835
|
+
expect(action).to \
|
836
|
+
match(/^Boltless \[tx:\w+:\d+ rq:\d+\] \([\d.]+ms\) .*/)
|
837
|
+
end
|
838
|
+
|
839
|
+
context 'with read access mode' do
|
840
|
+
let(:instance) { new_instance[access_mode: :read] }
|
841
|
+
|
842
|
+
it 'includes the access mode of the request' do
|
843
|
+
expect(action).to include('[tx:read:')
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
context 'with write access mode' do
|
848
|
+
let(:instance) { new_instance[access_mode: :write] }
|
849
|
+
|
850
|
+
it 'includes the access mode of the request' do
|
851
|
+
expect(action).to include('[tx:write:')
|
852
|
+
end
|
853
|
+
end
|
854
|
+
|
855
|
+
context 'with a transaction identifier' do
|
856
|
+
it 'includes the transaction identifier inside the tag' do
|
857
|
+
expect(action).to include('[tx:write:594 ')
|
858
|
+
end
|
859
|
+
|
860
|
+
it 'includes the current request count' do
|
861
|
+
instance.instance_variable_set(:@requests_done, 8)
|
862
|
+
expect(action).to include('[tx:write:594 rq:8]')
|
863
|
+
end
|
864
|
+
end
|
865
|
+
|
866
|
+
context 'without a transaction identifier' do
|
867
|
+
let(:tx_id) { nil }
|
868
|
+
|
869
|
+
it 'falls back to the one-shot transaction identifier' do
|
870
|
+
expect(action).to include('[tx:write:one-shot]')
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
context 'with a duration' do
|
875
|
+
it 'includes the formatted duration' do
|
876
|
+
expect(action).to include('] (9.5ms) RETURN')
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
context 'without a duration' do
|
881
|
+
let(:duration) { nil }
|
882
|
+
|
883
|
+
it 'skips the duration between tag and statement' do
|
884
|
+
expect(action).to include('] RETURN')
|
885
|
+
end
|
886
|
+
end
|
887
|
+
|
888
|
+
context 'with a single statement' do
|
889
|
+
it 'returns a string with a single line' do
|
890
|
+
expect(action.lines.count).to be_eql(1)
|
891
|
+
end
|
892
|
+
|
893
|
+
it 'resolves the Cypher statement with the parameters' do
|
894
|
+
expect(action).to include('RETURN 2')
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
context 'with multiple statements' do
|
899
|
+
let(:statements) do
|
900
|
+
[
|
901
|
+
{
|
902
|
+
statement: 'RETURN $arg',
|
903
|
+
parameters: { arg: 2 }
|
904
|
+
},
|
905
|
+
{
|
906
|
+
statement: 'MATCH (n:User { name: $name }) RETURN count(n)',
|
907
|
+
parameters: { name: 'Klaus' }
|
908
|
+
}
|
909
|
+
]
|
910
|
+
end
|
911
|
+
|
912
|
+
it 'returns a string with two lines' do
|
913
|
+
expect(action.lines.count).to be_eql(2)
|
914
|
+
end
|
915
|
+
|
916
|
+
it 'resolves the Cypher statement with the parameters (1)' do
|
917
|
+
expect(action).to include('RETURN 2')
|
918
|
+
end
|
919
|
+
|
920
|
+
it 'resolves the Cypher statement with the parameters (2)' do
|
921
|
+
expect(action).to \
|
922
|
+
include('MATCH (n:User { name: "Klaus" }) RETURN count(n)')
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
context 'with a mult-line Cypher statement' do
|
927
|
+
let(:statements) do
|
928
|
+
[
|
929
|
+
{
|
930
|
+
statement: <<~CYPHER,
|
931
|
+
MATCH (n:User { name: $name })
|
932
|
+
RETURN count(n) // Nice comment
|
933
|
+
CYPHER
|
934
|
+
parameters: { name: 'Klaus' }
|
935
|
+
}
|
936
|
+
]
|
937
|
+
end
|
938
|
+
|
939
|
+
it 'flattens multi-line Cypher statements to a single line each' do
|
940
|
+
expect(action).to \
|
941
|
+
include('MATCH (n:User { name: "Klaus" }) RETURN count(n)')
|
942
|
+
end
|
943
|
+
end
|
944
|
+
end
|
945
|
+
end
|
946
|
+
# rubocop:enable RSpec/NestedGroups
|