boltless 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Guardfile +44 -0
  4. data/Makefile +138 -0
  5. data/Rakefile +26 -0
  6. data/docker-compose.yml +19 -0
  7. data/lib/boltless/configuration.rb +69 -0
  8. data/lib/boltless/errors/invalid_json_error.rb +9 -0
  9. data/lib/boltless/errors/request_error.rb +24 -0
  10. data/lib/boltless/errors/response_error.rb +30 -0
  11. data/lib/boltless/errors/transaction_begin_error.rb +9 -0
  12. data/lib/boltless/errors/transaction_in_bad_state_error.rb +11 -0
  13. data/lib/boltless/errors/transaction_not_found_error.rb +11 -0
  14. data/lib/boltless/errors/transaction_rollback_error.rb +26 -0
  15. data/lib/boltless/extensions/configuration_handling.rb +37 -0
  16. data/lib/boltless/extensions/connection_pool.rb +127 -0
  17. data/lib/boltless/extensions/operations.rb +175 -0
  18. data/lib/boltless/extensions/transactions.rb +301 -0
  19. data/lib/boltless/extensions/utilities.rb +187 -0
  20. data/lib/boltless/request.rb +386 -0
  21. data/lib/boltless/result.rb +98 -0
  22. data/lib/boltless/result_row.rb +90 -0
  23. data/lib/boltless/statement_collector.rb +40 -0
  24. data/lib/boltless/transaction.rb +234 -0
  25. data/lib/boltless/version.rb +23 -0
  26. data/lib/boltless.rb +36 -0
  27. data/spec/benchmark/transfer.rb +57 -0
  28. data/spec/boltless/extensions/configuration_handling_spec.rb +39 -0
  29. data/spec/boltless/extensions/connection_pool_spec.rb +131 -0
  30. data/spec/boltless/extensions/operations_spec.rb +189 -0
  31. data/spec/boltless/extensions/transactions_spec.rb +418 -0
  32. data/spec/boltless/extensions/utilities_spec.rb +546 -0
  33. data/spec/boltless/request_spec.rb +946 -0
  34. data/spec/boltless/result_row_spec.rb +161 -0
  35. data/spec/boltless/result_spec.rb +127 -0
  36. data/spec/boltless/statement_collector_spec.rb +45 -0
  37. data/spec/boltless/transaction_spec.rb +601 -0
  38. data/spec/boltless_spec.rb +11 -0
  39. data/spec/fixtures/files/raw_result.yml +21 -0
  40. data/spec/fixtures/files/raw_result_with_graph_result.yml +48 -0
  41. data/spec/fixtures/files/raw_result_with_meta.yml +11 -0
  42. data/spec/fixtures/files/raw_result_with_stats.yml +26 -0
  43. data/spec/spec_helper.rb +89 -0
  44. 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