elasticsearch-transport 5.0.5 → 6.8.3

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 (42) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +5 -0
  3. data/README.md +88 -34
  4. data/Rakefile +14 -17
  5. data/elasticsearch-transport.gemspec +45 -63
  6. data/lib/elasticsearch/transport/client.rb +207 -67
  7. data/lib/elasticsearch/transport/redacted.rb +79 -0
  8. data/lib/elasticsearch/transport/transport/base.rb +28 -14
  9. data/lib/elasticsearch/transport/transport/connections/collection.rb +4 -0
  10. data/lib/elasticsearch/transport/transport/connections/connection.rb +5 -1
  11. data/lib/elasticsearch/transport/transport/connections/selector.rb +4 -0
  12. data/lib/elasticsearch/transport/transport/errors.rb +4 -0
  13. data/lib/elasticsearch/transport/transport/http/curb.rb +7 -2
  14. data/lib/elasticsearch/transport/transport/http/faraday.rb +11 -8
  15. data/lib/elasticsearch/transport/transport/http/manticore.rb +6 -1
  16. data/lib/elasticsearch/transport/transport/response.rb +4 -0
  17. data/lib/elasticsearch/transport/transport/serializer/multi_json.rb +4 -0
  18. data/lib/elasticsearch/transport/transport/sniffer.rb +31 -3
  19. data/lib/elasticsearch/transport/version.rb +5 -1
  20. data/lib/elasticsearch/transport.rb +5 -0
  21. data/lib/elasticsearch-transport.rb +4 -0
  22. data/spec/elasticsearch/transport/base_spec.rb +260 -0
  23. data/spec/elasticsearch/transport/client_spec.rb +1063 -0
  24. data/spec/elasticsearch/transport/meta_header_spec.rb +214 -0
  25. data/spec/elasticsearch/transport/sniffer_spec.rb +269 -0
  26. data/spec/spec_helper.rb +72 -0
  27. data/test/integration/transport_test.rb +9 -5
  28. data/test/profile/client_benchmark_test.rb +23 -25
  29. data/test/test_helper.rb +10 -0
  30. data/test/unit/connection_collection_test.rb +4 -0
  31. data/test/unit/connection_selector_test.rb +4 -0
  32. data/test/unit/connection_test.rb +4 -0
  33. data/test/unit/response_test.rb +5 -1
  34. data/test/unit/serializer_test.rb +4 -0
  35. data/test/unit/transport_base_test.rb +21 -1
  36. data/test/unit/transport_curb_test.rb +12 -0
  37. data/test/unit/transport_faraday_test.rb +16 -0
  38. data/test/unit/transport_manticore_test.rb +11 -0
  39. metadata +90 -76
  40. data/test/integration/client_test.rb +0 -237
  41. data/test/unit/client_test.rb +0 -366
  42. data/test/unit/sniffer_test.rb +0 -179
@@ -0,0 +1,1063 @@
1
+ # Licensed to Elasticsearch B.V under one or more agreements.
2
+ # Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3
+ # See the LICENSE file in the project root for more information
4
+
5
+ # Licensed to Elasticsearch B.V. under one or more contributor
6
+ # license agreements. See the NOTICE file distributed with
7
+ # this work for additional information regarding copyright
8
+ # ownership. Elasticsearch B.V. licenses this file to you under
9
+ # the Apache License, Version 2.0 (the "License"); you may
10
+ # not use this file except in compliance with the License.
11
+ # You may obtain a copy of the License at
12
+ #
13
+ # http://www.apache.org/licenses/LICENSE-2.0
14
+ #
15
+ # Unless required by applicable law or agreed to in writing,
16
+ # software distributed under the License is distributed on an
17
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18
+ # KIND, either express or implied. See the License for the
19
+ # specific language governing permissions and limitations
20
+ # under the License.
21
+
22
+ require 'spec_helper'
23
+
24
+ describe Elasticsearch::Transport::Client do
25
+
26
+ let(:client) do
27
+ described_class.new.tap do |_client|
28
+ allow(_client).to receive(:__build_connections)
29
+ end
30
+ end
31
+
32
+ it 'is aliased as Elasticsearch::Client' do
33
+ expect(Elasticsearch::Client.new).to be_a(described_class)
34
+ end
35
+
36
+ it 'has a default transport' do
37
+ expect(client.transport).to be_a(Elasticsearch::Transport::Client::DEFAULT_TRANSPORT_CLASS)
38
+ end
39
+
40
+ it 'uses Faraday as the default transport' do
41
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/Faraday/)
42
+ end
43
+
44
+ it 'sets the \'Content-Type\' header to \'application/json\' by default' do
45
+ expect(client.transport.options[:transport_options][:headers]['Content-Type']).to eq('application/json')
46
+ end
47
+
48
+ it 'uses localhost by default' do
49
+ expect(client.transport.hosts[0][:host]).to eq('localhost')
50
+ end
51
+
52
+ context 'when an encoded api_key is provided' do
53
+ let(:client) do
54
+ described_class.new(api_key: 'an_api_key')
55
+ end
56
+ let(:authorization_header) do
57
+ client.transport.connections.first.connection.headers['Authorization']
58
+ end
59
+
60
+ it 'Adds the ApiKey header to the connection' do
61
+ expect(authorization_header).to eq('ApiKey an_api_key')
62
+ end
63
+ end
64
+
65
+ context 'when an un-encoded api_key is provided' do
66
+ let(:client) do
67
+ described_class.new(api_key: { id: 'my_id', api_key: 'my_api_key' })
68
+ end
69
+ let(:authorization_header) do
70
+ client.transport.connections.first.connection.headers['Authorization']
71
+ end
72
+
73
+ it 'Adds the ApiKey header to the connection' do
74
+ expect(authorization_header).to eq("ApiKey #{Base64.strict_encode64('my_id:my_api_key')}")
75
+ end
76
+ end
77
+
78
+ context 'when basic auth and api_key are provided' do
79
+ let(:client) do
80
+ described_class.new(
81
+ api_key: { id: 'my_id', api_key: 'my_api_key' },
82
+ host: 'http://elastic:password@localhost:9200'
83
+ )
84
+ end
85
+ let(:authorization_header) do
86
+ client.transport.connections.first.connection.headers['Authorization']
87
+ end
88
+
89
+ it 'removes basic auth credentials' do
90
+ expect(authorization_header).not_to match(/^Basic/)
91
+ expect(authorization_header).to match(/^ApiKey/)
92
+ end
93
+ end
94
+
95
+ describe 'adapter' do
96
+ context 'when no adapter is specified' do
97
+ let(:adapter) do
98
+ client.transport.connections.all.first.connection.builder.adapter
99
+ end
100
+
101
+ it 'uses Faraday NetHttp' do
102
+ expect(adapter).to eq Faraday::Adapter::NetHttp
103
+ end
104
+ end
105
+
106
+ context 'when the adapter is patron' do
107
+ let(:adapter) do
108
+ client.transport.connections.all.first.connection.builder.adapter
109
+ end
110
+
111
+ let(:client) do
112
+ described_class.new(adapter: :patron, enable_meta_header: false)
113
+ end
114
+
115
+ it 'uses Faraday with the adapter' do
116
+ expect(adapter).to eq Faraday::Adapter::Patron
117
+ end
118
+ end
119
+
120
+ context 'when the adapter is typhoeus' do
121
+ let(:adapter) do
122
+ client.transport.connections.all.first.connection.builder.adapter
123
+ end
124
+
125
+ let(:client) do
126
+ described_class.new(adapter: :typhoeus, enable_meta_header: false)
127
+ end
128
+
129
+ it 'uses Faraday with the adapter' do
130
+ expect(adapter).to eq Faraday::Adapter::Typhoeus
131
+ end
132
+ end
133
+
134
+ context 'when the adapter is specified as a string key' do
135
+ let(:adapter) do
136
+ client.transport.connections.all.first.connection.builder.adapter
137
+ end
138
+
139
+ let(:client) do
140
+ described_class.new('adapter' => :patron, enable_meta_header: false)
141
+ end
142
+
143
+ it 'uses Faraday with the adapter' do
144
+ expect(adapter).to eq Faraday::Adapter::Patron
145
+ end
146
+ end
147
+
148
+ context 'when the adapter can be detected', unless: jruby? do
149
+
150
+ around do |example|
151
+ require 'patron'; load 'patron.rb'
152
+ example.run
153
+ end
154
+
155
+ let(:adapter) do
156
+ client.transport.connections.all.first.connection.builder.adapter
157
+ end
158
+
159
+ it 'uses the detected adapter' do
160
+ expect(adapter).to eq Faraday::Adapter::Patron
161
+ end
162
+ end
163
+
164
+ context 'when the Faraday adapter is configured' do
165
+
166
+ let(:client) do
167
+ described_class.new do |faraday|
168
+ faraday.adapter :patron
169
+ faraday.response :logger
170
+ end
171
+ end
172
+
173
+ let(:adapter) do
174
+ client.transport.connections.all.first.connection.builder.adapter
175
+ end
176
+
177
+ let(:handlers) do
178
+ client.transport.connections.all.first.connection.builder.handlers
179
+ end
180
+
181
+ it 'sets the adapter' do
182
+ expect(adapter).to eq Faraday::Adapter::Patron
183
+ end
184
+
185
+ it 'sets the logger' do
186
+ expect(handlers).to include(Faraday::Response::Logger)
187
+ end
188
+ end
189
+ end
190
+
191
+ shared_examples_for 'a client that extracts hosts' do
192
+
193
+ context 'when the hosts are a String' do
194
+
195
+ let(:host) do
196
+ 'myhost'
197
+ end
198
+
199
+ it 'extracts the host' do
200
+ expect(hosts[0][:host]).to eq('myhost')
201
+ expect(hosts[0][:protocol]).to eq('http')
202
+ expect(hosts[0][:port]).to be(9200)
203
+ end
204
+
205
+ context 'when a path is specified' do
206
+
207
+ let(:host) do
208
+ 'https://myhost:8080/api'
209
+ end
210
+
211
+ it 'extracts the host' do
212
+ expect(hosts[0][:host]).to eq('myhost')
213
+ expect(hosts[0][:scheme]).to eq('https')
214
+ expect(hosts[0][:path]).to eq('/api')
215
+ expect(hosts[0][:port]).to be(8080)
216
+ end
217
+ end
218
+
219
+ context 'when a scheme is specified' do
220
+
221
+ let(:host) do
222
+ 'https://myhost:8080'
223
+ end
224
+
225
+ it 'extracts the host' do
226
+ expect(hosts[0][:host]).to eq('myhost')
227
+ expect(hosts[0][:scheme]).to eq('https')
228
+ expect(hosts[0][:port]).to be(8080)
229
+ end
230
+ end
231
+
232
+ context 'when credentials are specified' do
233
+ let(:host) do
234
+ 'http://USERNAME:PASSWORD@myhost:8080'
235
+ end
236
+
237
+ it 'extracts the host' do
238
+ expect(hosts[0][:host]).to eq('myhost')
239
+ expect(hosts[0][:scheme]).to eq('http')
240
+ expect(hosts[0][:user]).to eq('USERNAME')
241
+ expect(hosts[0][:password]).to eq('PASSWORD')
242
+ expect(hosts[0][:port]).to be(8080)
243
+ end
244
+ end
245
+
246
+ context 'when there is a trailing slash' do
247
+
248
+ let(:host) do
249
+ 'http://myhost/'
250
+ end
251
+
252
+ it 'extracts the host' do
253
+ expect(hosts[0][:host]).to eq('myhost')
254
+ expect(hosts[0][:scheme]).to eq('http')
255
+ expect(hosts[0][:path]).to eq('')
256
+ end
257
+ end
258
+
259
+ context 'when there is a trailing slash with a path' do
260
+
261
+ let(:host) do
262
+ 'http://myhost/foo/bar/'
263
+ end
264
+
265
+ it 'extracts the host' do
266
+ expect(hosts[0][:host]).to eq('myhost')
267
+ expect(hosts[0][:scheme]).to eq('http')
268
+ expect(hosts[0][:path]).to eq('/foo/bar')
269
+ end
270
+ end
271
+ end
272
+
273
+ context 'when the hosts are a Hash' do
274
+
275
+ let(:host) do
276
+ { :host => 'myhost', :scheme => 'https' }
277
+ end
278
+
279
+ it 'extracts the host' do
280
+ expect(hosts[0][:host]).to eq('myhost')
281
+ expect(hosts[0][:scheme]).to eq('https')
282
+ expect(hosts[0][:port]).to be(9200)
283
+ end
284
+
285
+ context 'when the port is specified as a String' do
286
+
287
+ let(:host) do
288
+ { host: 'myhost', scheme: 'https', port: '443' }
289
+ end
290
+
291
+ it 'extracts the host' do
292
+ expect(hosts[0][:host]).to eq('myhost')
293
+ expect(hosts[0][:scheme]).to eq('https')
294
+ expect(hosts[0][:port]).to be(443)
295
+ end
296
+ end
297
+
298
+ context 'when the port is specified as an Integer' do
299
+
300
+ let(:host) do
301
+ { host: 'myhost', scheme: 'https', port: 443 }
302
+ end
303
+
304
+ it 'extracts the host' do
305
+ expect(hosts[0][:host]).to eq('myhost')
306
+ expect(hosts[0][:scheme]).to eq('https')
307
+ expect(hosts[0][:port]).to be(443)
308
+ end
309
+ end
310
+ end
311
+
312
+ context 'when the hosts are a Hashie:Mash' do
313
+
314
+ let(:host) do
315
+ Hashie::Mash.new(host: 'myhost', scheme: 'https')
316
+ end
317
+
318
+ it 'extracts the host' do
319
+ expect(hosts[0][:host]).to eq('myhost')
320
+ expect(hosts[0][:scheme]).to eq('https')
321
+ expect(hosts[0][:port]).to be(9200)
322
+ end
323
+
324
+ context 'when the port is specified as a String' do
325
+
326
+ let(:host) do
327
+ Hashie::Mash.new(host: 'myhost', scheme: 'https', port: '443')
328
+ end
329
+
330
+ it 'extracts the host' do
331
+ expect(hosts[0][:host]).to eq('myhost')
332
+ expect(hosts[0][:scheme]).to eq('https')
333
+ expect(hosts[0][:port]).to be(443)
334
+ end
335
+ end
336
+
337
+ context 'when the port is specified as an Integer' do
338
+
339
+ let(:host) do
340
+ Hashie::Mash.new(host: 'myhost', scheme: 'https', port: 443)
341
+ end
342
+
343
+ it 'extracts the host' do
344
+ expect(hosts[0][:host]).to eq('myhost')
345
+ expect(hosts[0][:scheme]).to eq('https')
346
+ expect(hosts[0][:port]).to be(443)
347
+ end
348
+ end
349
+ end
350
+
351
+ context 'when the hosts are an array' do
352
+
353
+ context 'when there is one host' do
354
+
355
+ let(:host) do
356
+ ['myhost']
357
+ end
358
+
359
+ it 'extracts the host' do
360
+ expect(hosts[0][:host]).to eq('myhost')
361
+ expect(hosts[0][:protocol]).to eq('http')
362
+ expect(hosts[0][:port]).to be(9200)
363
+ end
364
+ end
365
+
366
+ context 'when there is more than one host' do
367
+
368
+ let(:host) do
369
+ ['host1', 'host2']
370
+ end
371
+
372
+ it 'extracts the host' do
373
+ expect(hosts[0][:host]).to eq('host1')
374
+ expect(hosts[0][:protocol]).to eq('http')
375
+ expect(hosts[0][:port]).to be(9200)
376
+ expect(hosts[1][:host]).to eq('host2')
377
+ expect(hosts[1][:protocol]).to eq('http')
378
+ expect(hosts[1][:port]).to be(9200)
379
+ end
380
+ end
381
+
382
+ context 'when ports are also specified' do
383
+
384
+ let(:host) do
385
+ ['host1:1000', 'host2:2000']
386
+ end
387
+
388
+ it 'extracts the host' do
389
+ expect(hosts[0][:host]).to eq('host1')
390
+ expect(hosts[0][:protocol]).to eq('http')
391
+ expect(hosts[0][:port]).to be(1000)
392
+ expect(hosts[1][:host]).to eq('host2')
393
+ expect(hosts[1][:protocol]).to eq('http')
394
+ expect(hosts[1][:port]).to be(2000)
395
+ end
396
+ end
397
+ end
398
+
399
+ context 'when the hosts is an instance of URI' do
400
+
401
+ let(:host) do
402
+ URI.parse('https://USERNAME:PASSWORD@myhost:4430')
403
+ end
404
+
405
+ it 'extracts the host' do
406
+ expect(hosts[0][:host]).to eq('myhost')
407
+ expect(hosts[0][:scheme]).to eq('https')
408
+ expect(hosts[0][:port]).to be(4430)
409
+ expect(hosts[0][:user]).to eq('USERNAME')
410
+ expect(hosts[0][:password]).to eq('PASSWORD')
411
+ end
412
+ end
413
+
414
+ context 'when the hosts is invalid' do
415
+
416
+ let(:host) do
417
+ 123
418
+ end
419
+
420
+ it 'extracts the host' do
421
+ expect {
422
+ hosts
423
+ }.to raise_exception(ArgumentError)
424
+ end
425
+ end
426
+ end
427
+
428
+ context 'when hosts are specified with the \'host\' key' do
429
+
430
+ let(:client) do
431
+ described_class.new(hosts: ['host1', 'host2', 'host3', 'host4'], randomize_hosts: true)
432
+ end
433
+
434
+ let(:hosts) do
435
+ client.transport.hosts
436
+ end
437
+
438
+ it 'sets the hosts in random order' do
439
+ expect(hosts.all? { |host| client.transport.hosts.include?(host) }).to be(true)
440
+ end
441
+ end
442
+
443
+ context 'when hosts are specified with the \'host\' key' do
444
+
445
+ let(:client) do
446
+ described_class.new(host: host)
447
+ end
448
+
449
+ let(:hosts) do
450
+ client.transport.hosts
451
+ end
452
+
453
+ it_behaves_like 'a client that extracts hosts'
454
+ end
455
+
456
+ context 'when hosts are specified with the \'hosts\' key' do
457
+
458
+ let(:client) do
459
+ described_class.new(host: host)
460
+ end
461
+
462
+ let(:hosts) do
463
+ client.transport.hosts
464
+ end
465
+
466
+ it_behaves_like 'a client that extracts hosts'
467
+ end
468
+
469
+ context 'when hosts are specified with the \'url\' key' do
470
+
471
+ let(:client) do
472
+ described_class.new(host: host)
473
+ end
474
+
475
+ let(:hosts) do
476
+ client.transport.hosts
477
+ end
478
+
479
+ it_behaves_like 'a client that extracts hosts'
480
+ end
481
+
482
+ context 'when hosts are specified with the \'urls\' key' do
483
+
484
+ let(:client) do
485
+ described_class.new(host: host)
486
+ end
487
+
488
+ let(:hosts) do
489
+ client.transport.hosts
490
+ end
491
+
492
+ it_behaves_like 'a client that extracts hosts'
493
+ end
494
+
495
+ context 'when the URL is set in the ELASTICSEARCH_URL environment variable' do
496
+
497
+ context 'when there is only one host specified' do
498
+
499
+ around do |example|
500
+ before_url = ENV['ELASTICSEARCH_URL']
501
+ ENV['ELASTICSEARCH_URL'] = 'example.com'
502
+ example.run
503
+ ENV['ELASTICSEARCH_URL'] = before_url
504
+ end
505
+
506
+ it 'sets the host' do
507
+ expect(client.transport.hosts[0][:host]).to eq('example.com')
508
+ expect(client.transport.hosts.size).to eq(1)
509
+ end
510
+ end
511
+
512
+ context 'when mutliple hosts are specified as a comma-separated String list' do
513
+
514
+ around do |example|
515
+ before_url = ENV['ELASTICSEARCH_URL']
516
+ ENV['ELASTICSEARCH_URL'] = 'example.com, other.com'
517
+ example.run
518
+ ENV['ELASTICSEARCH_URL'] = before_url
519
+ end
520
+
521
+ it 'sets the hosts' do
522
+ expect(client.transport.hosts[0][:host]).to eq('example.com')
523
+ expect(client.transport.hosts[1][:host]).to eq('other.com')
524
+ expect(client.transport.hosts.size).to eq(2)
525
+ end
526
+ end
527
+ end
528
+
529
+ context 'when options are defined' do
530
+
531
+ context 'when scheme is specified' do
532
+
533
+ let(:client) do
534
+ described_class.new(scheme: 'https')
535
+ end
536
+
537
+ it 'sets the scheme' do
538
+ expect(client.transport.connections[0].full_url('')).to match(/https/)
539
+ end
540
+ end
541
+
542
+ context 'when user and password are specified' do
543
+
544
+ let(:client) do
545
+ described_class.new(user: 'USERNAME', password: 'PASSWORD')
546
+ end
547
+
548
+ it 'sets the user and password' do
549
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
550
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
551
+ end
552
+
553
+ context 'when the connections are reloaded' do
554
+
555
+ before do
556
+ allow(client.transport.sniffer).to receive(:hosts).and_return([{ host: 'foobar', port: 4567, id: 'foobar4567' }])
557
+ client.transport.reload_connections!
558
+ end
559
+
560
+ it 'sets keeps user and password' do
561
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
562
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
563
+ expect(client.transport.connections[0].full_url('')).to match(/foobar/)
564
+ end
565
+ end
566
+ end
567
+
568
+ context 'when port is specified' do
569
+
570
+ let(:client) do
571
+ described_class.new(host: 'node1', port: 1234)
572
+ end
573
+
574
+ it 'sets the port' do
575
+ expect(client.transport.connections[0].full_url('')).to match(/1234/)
576
+ end
577
+ end
578
+
579
+ context 'when the log option is true' do
580
+
581
+ let(:client) do
582
+ described_class.new(log: true)
583
+ end
584
+
585
+ it 'has a default logger for transport' do
586
+ expect(client.transport.logger.info).to eq(described_class::DEFAULT_LOGGER.call.info)
587
+ end
588
+ end
589
+
590
+ context 'when the trace option is true' do
591
+
592
+ let(:client) do
593
+ described_class.new(trace: true)
594
+ end
595
+
596
+ it 'has a default logger for transport' do
597
+ expect(client.transport.tracer.info).to eq(described_class::DEFAULT_TRACER.call.info)
598
+ end
599
+ end
600
+
601
+ context 'when a custom transport class is specified' do
602
+
603
+ let(:transport_class) do
604
+ Class.new { def initialize(*); end }
605
+ end
606
+
607
+ let(:client) do
608
+ described_class.new(transport_class: transport_class)
609
+ end
610
+
611
+ it 'allows the custom transport class to be defined' do
612
+ expect(client.transport).to be_a(transport_class)
613
+ end
614
+ end
615
+
616
+ context 'when a custom transport instance is specified' do
617
+
618
+ let(:transport_instance) do
619
+ Class.new { def initialize(*); end }.new
620
+ end
621
+
622
+ let(:client) do
623
+ described_class.new(transport: transport_instance)
624
+ end
625
+
626
+ it 'allows the custom transport class to be defined' do
627
+ expect(client.transport).to be(transport_instance)
628
+ end
629
+ end
630
+
631
+ context 'when \'transport_options\' are defined' do
632
+
633
+ let(:client) do
634
+ described_class.new(transport_options: { request: { timeout: 1 } })
635
+ end
636
+
637
+ it 'sets the options on the transport' do
638
+ expect(client.transport.options[:transport_options][:request]).to eq(timeout: 1)
639
+ end
640
+ end
641
+
642
+ context 'when \'request_timeout\' is defined' do
643
+
644
+ let(:client) do
645
+ described_class.new(request_timeout: 120)
646
+ end
647
+
648
+ it 'sets the options on the transport' do
649
+ expect(client.transport.options[:transport_options][:request]).to eq(timeout: 120)
650
+ end
651
+ end
652
+ end
653
+
654
+ describe '#perform_request' do
655
+
656
+ let(:transport_instance) do
657
+ Class.new { def initialize(*); end }.new
658
+ end
659
+
660
+ let(:client) do
661
+ described_class.new(transport: transport_instance)
662
+ end
663
+
664
+ it 'delegates performing requests to the transport' do
665
+ expect(transport_instance).to receive(:perform_request).and_return(true)
666
+ expect(client.perform_request('GET', '/')).to be(true)
667
+ end
668
+
669
+ context 'when the \'send_get_body_as\' option is specified' do
670
+
671
+ let(:client) do
672
+ described_class.new(transport: transport_instance, :send_get_body_as => 'POST')
673
+ end
674
+
675
+ before do
676
+ expect(transport_instance).to receive(:perform_request).with('POST', '/', {},
677
+ '{"foo":"bar"}',
678
+ '{"Content-Type":"application/x-ndjson"}').and_return(true)
679
+ end
680
+
681
+ let(:request) do
682
+ client.perform_request('POST', '/', {}, '{"foo":"bar"}', '{"Content-Type":"application/x-ndjson"}')
683
+ end
684
+
685
+ it 'sets the option' do
686
+ expect(request).to be(true)
687
+ end
688
+ end
689
+
690
+ context 'when x-opaque-id is set' do
691
+ let(:client) { described_class.new(host: hosts) }
692
+
693
+ it 'uses x-opaque-id on a request' do
694
+ expect(client.perform_request('GET', '/', { opaque_id: '12345' }).headers['x-opaque-id']).to eq('12345')
695
+ end
696
+ end
697
+
698
+ context 'when an x-opaque-id prefix is set on initialization' do
699
+ let(:prefix) { 'elastic_cloud' }
700
+ let(:client) do
701
+ described_class.new(host: hosts, opaque_id_prefix: prefix)
702
+ end
703
+
704
+ it 'uses x-opaque-id on a request' do
705
+ expect(client.perform_request('GET', '/', { opaque_id: '12345' }).headers['x-opaque-id']).to eq("#{prefix}12345")
706
+ end
707
+
708
+ context 'when using an API call' do
709
+ let(:client) { described_class.new(host: hosts) }
710
+
711
+ it 'doesnae raise an ArgumentError' do
712
+ expect { client.search(opaque_id: 'no_error') }.not_to raise_error
713
+ end
714
+
715
+ it 'uses X-Opaque-Id in the header' do
716
+ allow(client).to receive(:perform_request) { OpenStruct.new(body: '') }
717
+ expect { client.search(opaque_id: 'opaque_id') }.not_to raise_error
718
+ expect(client).to have_received(:perform_request)
719
+ .with('GET', '_search', { opaque_id: 'opaque_id' }, nil)
720
+ end
721
+ end
722
+ end
723
+ end
724
+
725
+ context 'when the client connects to Elasticsearch' do
726
+ let(:logger) do
727
+ Logger.new(STDERR).tap do |logger|
728
+ logger.formatter = proc do |severity, datetime, progname, msg|
729
+ color = case severity
730
+ when /INFO/ then :green
731
+ when /ERROR|WARN|FATAL/ then :red
732
+ when /DEBUG/ then :cyan
733
+ else :white
734
+ end
735
+ ANSI.ansi(severity[0] + ' ', color, :faint) + ANSI.ansi(msg, :white, :faint) + "\n"
736
+ end
737
+ end unless ENV['QUIET']
738
+ end
739
+
740
+ let(:port) do
741
+ (ENV['TEST_CLUSTER_PORT'] || 9250).to_i
742
+ end
743
+
744
+ let(:transport_options) do
745
+ {}
746
+ end
747
+
748
+ let(:options) do
749
+ {}
750
+ end
751
+
752
+ let(:client) do
753
+ described_class.new({ host: hosts, logger: logger }.merge!(transport_options: transport_options).merge!(options))
754
+ end
755
+
756
+ context 'when a request is made' do
757
+
758
+ let!(:response) do
759
+ client.perform_request('GET', '_cluster/health')
760
+ end
761
+
762
+ it 'connects to the cluster' do
763
+ expect(response.body['number_of_nodes']).to be >(1)
764
+ end
765
+ end
766
+
767
+ describe '#initialize' do
768
+
769
+ context 'when options are specified' do
770
+
771
+ let(:transport_options) do
772
+ { headers: { accept: 'application/yaml', content_type: 'application/yaml' } }
773
+ end
774
+
775
+ let(:response) do
776
+ client.perform_request('GET', '_cluster/health')
777
+ end
778
+
779
+ it 'applies the options to the client' do
780
+ expect(response.body).to match(/---\n/)
781
+ expect(response.headers['content-type']).to eq('application/yaml')
782
+ end
783
+ end
784
+
785
+ context 'when a block is provided' do
786
+
787
+ let(:client) do
788
+ Elasticsearch::Client.new(host: ELASTICSEARCH_HOSTS.first, logger: logger) do |client|
789
+ client.headers['Accept'] = 'application/yaml'
790
+ end
791
+ end
792
+
793
+ let(:response) do
794
+ client.perform_request('GET', '_cluster/health')
795
+ end
796
+
797
+ it 'executes the block' do
798
+ expect(response.body).to match(/---\n/)
799
+ expect(response.headers['content-type']).to eq('application/yaml')
800
+ end
801
+
802
+ context 'when the Faraday adapter is set in the block' do
803
+ let(:client) do
804
+ Elasticsearch::Client.new(host: ELASTICSEARCH_HOSTS.first, logger: logger) do |client|
805
+ client.adapter(:net_http_persistent)
806
+ end
807
+ end
808
+
809
+ let(:handler_name) do
810
+ client.transport.connections.first.connection.builder.adapter.name
811
+ end
812
+
813
+ let(:response) do
814
+ client.perform_request('GET', '_cluster/health')
815
+ end
816
+
817
+ it 'sets the adapter' do
818
+ expect(handler_name).to eq('Faraday::Adapter::NetHttpPersistent')
819
+ end
820
+
821
+ it 'uses the adapter to connect' do
822
+ expect(response.status).to eq(200)
823
+ end
824
+ end
825
+ end
826
+ end
827
+
828
+ describe '#options' do
829
+
830
+ context 'when retry_on_failure is true' do
831
+
832
+ context 'when a node is unreachable' do
833
+
834
+ let(:hosts) do
835
+ [ELASTICSEARCH_HOSTS.first, "foobar1", "foobar2"]
836
+ end
837
+
838
+ let(:options) do
839
+ { retry_on_failure: true }
840
+ end
841
+
842
+ let(:responses) do
843
+ 5.times.collect do
844
+ client.perform_request('GET', '_nodes/_local')
845
+ end
846
+ end
847
+
848
+ it 'retries on failure' do
849
+ expect(responses.all? { true }).to be(true)
850
+ end
851
+ end
852
+ end
853
+
854
+ context 'when retry_on_failure is an integer' do
855
+
856
+ let(:hosts) do
857
+ [ELASTICSEARCH_HOSTS.first, 'foobar1', 'foobar2', 'foobar3']
858
+ end
859
+
860
+ let(:options) do
861
+ { retry_on_failure: 1 }
862
+ end
863
+
864
+ it 'retries only the specified number of times' do
865
+ expect(client.perform_request('GET', '_nodes/_local'))
866
+ expect {
867
+ client.perform_request('GET', '_nodes/_local')
868
+ }.to raise_exception(Faraday::ConnectionFailed)
869
+ end
870
+ end
871
+
872
+ context 'when reload_on_failure is true' do
873
+
874
+ let(:hosts) do
875
+ [ELASTICSEARCH_HOSTS.first, 'foobar1', 'foobar2']
876
+ end
877
+
878
+ let(:options) do
879
+ { reload_on_failure: true }
880
+ end
881
+
882
+ let(:responses) do
883
+ 5.times.collect do
884
+ client.perform_request('GET', '_nodes/_local')
885
+ end
886
+ end
887
+
888
+ it 'reloads the connections' do
889
+ expect(client.transport.connections.size).to eq(3)
890
+ expect(responses.all? { true }).to be(true)
891
+ expect(client.transport.connections.size).to eq(2)
892
+ end
893
+ end
894
+
895
+ context 'when retry_on_status is specified' do
896
+
897
+ let(:options) do
898
+ { retry_on_status: 400 }
899
+ end
900
+
901
+ let(:logger) do
902
+ double('logger', :fatal => false)
903
+ end
904
+
905
+ before do
906
+ expect(logger).to receive(:warn).exactly(4).times
907
+ end
908
+
909
+ it 'retries when the status matches' do
910
+ expect {
911
+ client.perform_request('PUT', '_foobar')
912
+ }.to raise_exception(Elasticsearch::Transport::Transport::Errors::BadRequest)
913
+ end
914
+ end
915
+ end
916
+
917
+ describe '#perform_request' do
918
+
919
+ context 'when a request is made' do
920
+
921
+ before do
922
+ client.perform_request('DELETE', '_all')
923
+ client.perform_request('DELETE', 'myindex') rescue
924
+ client.perform_request('PUT', 'myindex', {}, { settings: { number_of_shards: 10 } })
925
+ client.perform_request('PUT', 'myindex/mydoc/1', { routing: 'XYZ', timeout: '1s' }, { foo: 'bar' })
926
+ client.perform_request('GET', '_cluster/health?wait_for_status=green', {})
927
+ end
928
+
929
+ let(:response) do
930
+ client.perform_request('GET', 'myindex/mydoc/1?routing=XYZ')
931
+ end
932
+
933
+ it 'handles paths and URL paramters' do
934
+ expect(response.status).to eq(200)
935
+ end
936
+
937
+ it 'returns response body' do
938
+ expect(response.body['_source']).to eq('foo' => 'bar')
939
+ end
940
+ end
941
+
942
+ context 'when an invalid url is specified' do
943
+
944
+ it 'raises an exception' do
945
+ expect {
946
+ client.perform_request('GET', 'myindex/mydoc/1?routing=FOOBARBAZ')
947
+ }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound)
948
+ end
949
+ end
950
+
951
+ context 'when the \'ignore\' parameter is specified' do
952
+
953
+ let(:response) do
954
+ client.perform_request('PUT', '_foobar', ignore: 400)
955
+ end
956
+
957
+ it 'exposes the status in the response' do
958
+ expect(response.status).to eq(400)
959
+ end
960
+
961
+ it 'exposes the body of the response' do
962
+ expect(response.body).to be_a(Hash)
963
+ expect(response.body.inspect).to match(/invalid_index_name_exception/)
964
+ end
965
+ end
966
+
967
+ context 'when request headers are specified' do
968
+
969
+ let(:response) do
970
+ client.perform_request('GET', '/', {}, nil, { 'Content-Type' => 'application/yaml' })
971
+ end
972
+
973
+ it 'passes them to the transport' do
974
+ expect(response.body).to match(/---/)
975
+ end
976
+ end
977
+
978
+ describe 'selector' do
979
+
980
+ context 'when the round-robin selector is used' do
981
+
982
+ let(:nodes) do
983
+ 3.times.collect do
984
+ client.perform_request('GET', '_nodes/_local').body['nodes'].to_a[0][1]['name']
985
+ end
986
+ end
987
+
988
+ let(:node_names) do
989
+ client.nodes.stats['nodes'].collect do |name, stats|
990
+ stats['name']
991
+ end
992
+ end
993
+
994
+ let(:expected_names) do
995
+ 3.times.collect do |i|
996
+ node_names[i % node_names.size]
997
+ end
998
+ end
999
+
1000
+ # it 'rotates nodes' do
1001
+ # pending 'Better way to detect rotating nodes'
1002
+ # expect(nodes).to eq(expected_names)
1003
+ # end
1004
+ end
1005
+ end
1006
+
1007
+ context 'when patron is used as an adapter', unless: jruby? do
1008
+
1009
+ before do
1010
+ require 'patron'
1011
+ end
1012
+
1013
+ let(:options) do
1014
+ { adapter: :patron }
1015
+ end
1016
+
1017
+ let(:adapter) do
1018
+ client.transport.connections.first.connection.builder.adapter
1019
+ end
1020
+
1021
+ it 'uses the patron connection handler' do
1022
+ expect(adapter).to eq('Faraday::Adapter::Patron')
1023
+ end
1024
+
1025
+ it 'keeps connections open' do
1026
+ response = client.perform_request('GET', '_nodes/stats/http')
1027
+ connections_before = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1028
+ client.transport.reload_connections!
1029
+ response = client.perform_request('GET', '_nodes/stats/http')
1030
+ connections_after = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1031
+ expect(connections_after).to be >= (connections_before)
1032
+ end
1033
+ end
1034
+
1035
+ context 'when typhoeus is used as an adapter', unless: jruby? do
1036
+ before do
1037
+ require 'typhoeus'
1038
+ end
1039
+
1040
+ let(:options) do
1041
+ { adapter: :typhoeus }
1042
+ end
1043
+
1044
+ let(:adapter) do
1045
+ client.transport.connections.first.connection.builder.adapter
1046
+ end
1047
+
1048
+ it 'uses the patron connection handler' do
1049
+ expect(adapter).to eq('Faraday::Adapter::Typhoeus')
1050
+ end
1051
+
1052
+ it 'keeps connections open' do
1053
+ response = client.perform_request('GET', '_nodes/stats/http')
1054
+ connections_before = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1055
+ client.transport.reload_connections!
1056
+ response = client.perform_request('GET', '_nodes/stats/http')
1057
+ connections_after = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1058
+ expect(connections_after).to be >= (connections_before)
1059
+ end
1060
+ end
1061
+ end
1062
+ end
1063
+ end