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.
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