opensearch-transport 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. checksums.yaml.gz.sig +3 -0
  3. data/.gitignore +17 -0
  4. data/Gemfile +47 -0
  5. data/LICENSE +202 -0
  6. data/README.md +551 -0
  7. data/Rakefile +89 -0
  8. data/lib/opensearch/transport/client.rb +354 -0
  9. data/lib/opensearch/transport/redacted.rb +84 -0
  10. data/lib/opensearch/transport/transport/base.rb +450 -0
  11. data/lib/opensearch/transport/transport/connections/collection.rb +136 -0
  12. data/lib/opensearch/transport/transport/connections/connection.rb +169 -0
  13. data/lib/opensearch/transport/transport/connections/selector.rb +101 -0
  14. data/lib/opensearch/transport/transport/errors.rb +100 -0
  15. data/lib/opensearch/transport/transport/http/curb.rb +140 -0
  16. data/lib/opensearch/transport/transport/http/faraday.rb +101 -0
  17. data/lib/opensearch/transport/transport/http/manticore.rb +188 -0
  18. data/lib/opensearch/transport/transport/loggable.rb +94 -0
  19. data/lib/opensearch/transport/transport/response.rb +46 -0
  20. data/lib/opensearch/transport/transport/serializer/multi_json.rb +62 -0
  21. data/lib/opensearch/transport/transport/sniffer.rb +111 -0
  22. data/lib/opensearch/transport/version.rb +31 -0
  23. data/lib/opensearch/transport.rb +46 -0
  24. data/lib/opensearch-transport.rb +27 -0
  25. data/opensearch-transport.gemspec +92 -0
  26. data/spec/opensearch/connections/collection_spec.rb +275 -0
  27. data/spec/opensearch/connections/selector_spec.rb +183 -0
  28. data/spec/opensearch/transport/base_spec.rb +313 -0
  29. data/spec/opensearch/transport/client_spec.rb +1818 -0
  30. data/spec/opensearch/transport/sniffer_spec.rb +284 -0
  31. data/spec/spec_helper.rb +99 -0
  32. data/test/integration/transport_test.rb +108 -0
  33. data/test/profile/client_benchmark_test.rb +141 -0
  34. data/test/test_helper.rb +97 -0
  35. data/test/unit/connection_test.rb +145 -0
  36. data/test/unit/response_test.rb +41 -0
  37. data/test/unit/serializer_test.rb +42 -0
  38. data/test/unit/transport_base_test.rb +673 -0
  39. data/test/unit/transport_curb_test.rb +143 -0
  40. data/test/unit/transport_faraday_test.rb +237 -0
  41. data/test/unit/transport_manticore_test.rb +191 -0
  42. data.tar.gz.sig +1 -0
  43. metadata +456 -0
  44. metadata.gz.sig +1 -0
@@ -0,0 +1,1818 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ #
3
+ # The OpenSearch Contributors require contributions made to
4
+ # this file be licensed under the Apache-2.0 license or a
5
+ # compatible open source license.
6
+ #
7
+ # Modifications Copyright OpenSearch Contributors. See
8
+ # GitHub history for details.
9
+ #
10
+ # Licensed to Elasticsearch B.V. under one or more contributor
11
+ # license agreements. See the NOTICE file distributed with
12
+ # this work for additional information regarding copyright
13
+ # ownership. Elasticsearch B.V. licenses this file to you under
14
+ # the Apache License, Version 2.0 (the "License"); you may
15
+ # not use this file except in compliance with the License.
16
+ # You may obtain a copy of the License at
17
+ #
18
+ # http://www.apache.org/licenses/LICENSE-2.0
19
+ #
20
+ # Unless required by applicable law or agreed to in writing,
21
+ # software distributed under the License is distributed on an
22
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
23
+ # KIND, either express or implied. See the License for the
24
+ # specific language governing permissions and limitations
25
+ # under the License.
26
+
27
+ require 'spec_helper'
28
+ require 'opensearch'
29
+
30
+ describe OpenSearch::Transport::Client do
31
+ let(:client) do
32
+ described_class.new.tap do |_client|
33
+ allow(_client).to receive(:__build_connections)
34
+ end
35
+ end
36
+
37
+ it 'has a default transport' do
38
+ expect(client.transport).to be_a(OpenSearch::Transport::Client::DEFAULT_TRANSPORT_CLASS)
39
+ end
40
+
41
+ it 'preserves the Faraday default user agent header' do
42
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/Faraday/)
43
+ end
44
+
45
+ it 'identifies the Ruby client in the User-Agent header' do
46
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/opensearch-ruby\/#{OpenSearch::Transport::VERSION}/)
47
+ end
48
+
49
+ it 'identifies the Ruby version in the User-Agent header' do
50
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RUBY_VERSION}/)
51
+ end
52
+
53
+ it 'identifies the host_os in the User-Agent header' do
54
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase}/)
55
+ end
56
+
57
+ it 'identifies the target_cpu in the User-Agent header' do
58
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RbConfig::CONFIG['target_cpu']}/)
59
+ end
60
+
61
+ it 'sets the \'Content-Type\' header to \'application/json\' by default' do
62
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
63
+ end
64
+
65
+ it 'uses localhost by default' do
66
+ expect(client.transport.hosts[0][:host]).to eq('localhost')
67
+ end
68
+
69
+ context 'when a User-Agent header is specified as client option' do
70
+ let(:client) do
71
+ described_class.new(transport_options: { headers: { 'User-Agent' => 'testing' } })
72
+ end
73
+
74
+ it 'sets the specified User-Agent header' do
75
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to eq('testing')
76
+ end
77
+ end
78
+
79
+ context 'when an encoded api_key is provided' do
80
+ let(:client) do
81
+ described_class.new(api_key: 'an_api_key')
82
+ end
83
+ let(:authorization_header) do
84
+ client.transport.connections.first.connection.headers['Authorization']
85
+ end
86
+
87
+ it 'Adds the ApiKey header to the connection' do
88
+ expect(authorization_header).to eq('ApiKey an_api_key')
89
+ end
90
+ end
91
+
92
+ context 'when an un-encoded api_key is provided' do
93
+ let(:client) do
94
+ described_class.new(api_key: { id: 'my_id', api_key: 'my_api_key' })
95
+ end
96
+ let(:authorization_header) do
97
+ client.transport.connections.first.connection.headers['Authorization']
98
+ end
99
+
100
+ it 'Adds the ApiKey header to the connection' do
101
+ expect(authorization_header).to eq("ApiKey #{Base64.strict_encode64('my_id:my_api_key')}")
102
+ end
103
+ end
104
+
105
+ context 'when basic auth and api_key are provided' do
106
+ let(:client) do
107
+ described_class.new(
108
+ api_key: { id: 'my_id', api_key: 'my_api_key' },
109
+ host: 'https://admin:admin@localhost:9200'
110
+ )
111
+ end
112
+ let(:authorization_header) do
113
+ client.transport.connections.first.connection.headers['Authorization']
114
+ end
115
+
116
+ it 'removes basic auth credentials' do
117
+ expect(authorization_header).not_to match(/^Basic/)
118
+ expect(authorization_header).to match(/^ApiKey/)
119
+ end
120
+ end
121
+
122
+ context 'when a user-agent header is specified as client option in lower-case' do
123
+
124
+ let(:client) do
125
+ described_class.new(transport_options: { headers: { 'user-agent' => 'testing' } })
126
+ end
127
+
128
+ it 'sets the specified User-Agent header' do
129
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to eq('testing')
130
+ end
131
+ end
132
+
133
+ context 'when a Content-Type header is specified as client option' do
134
+
135
+ let(:client) do
136
+ described_class.new(transport_options: { headers: { 'Content-Type' => 'testing' } })
137
+ end
138
+
139
+ it 'sets the specified Content-Type header' do
140
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('testing')
141
+ end
142
+ end
143
+
144
+ context 'when a content-type header is specified as client option in lower-case' do
145
+
146
+ let(:client) do
147
+ described_class.new(transport_options: { headers: { 'content-type' => 'testing' } })
148
+ end
149
+
150
+ it 'sets the specified Content-Type header' do
151
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('testing')
152
+ end
153
+ end
154
+
155
+ context 'when the Curb transport class is used', unless: jruby? do
156
+
157
+ let(:client) do
158
+ described_class.new(transport_class: OpenSearch::Transport::Transport::HTTP::Curb)
159
+ end
160
+
161
+ it 'preserves the Curb default user agent header' do
162
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/Curb/)
163
+ end
164
+
165
+ it 'identifies the Ruby client in the User-Agent header' do
166
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/opensearch-ruby\/#{OpenSearch::Transport::VERSION}/)
167
+ end
168
+
169
+ it 'identifies the Ruby version in the User-Agent header' do
170
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RUBY_VERSION}/)
171
+ end
172
+
173
+ it 'identifies the host_os in the User-Agent header' do
174
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RbConfig::CONFIG['host_os'].split('_').first[/[a-z]+/i].downcase}/)
175
+ end
176
+
177
+ it 'identifies the target_cpu in the User-Agent header' do
178
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to match(/#{RbConfig::CONFIG['target_cpu']}/)
179
+ end
180
+
181
+ it 'sets the \'Content-Type\' header to \'application/json\' by default' do
182
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
183
+ end
184
+
185
+ it 'uses localhost by default' do
186
+ expect(client.transport.hosts[0][:host]).to eq('localhost')
187
+ end
188
+
189
+ context 'when a User-Agent header is specified as a client option' do
190
+
191
+ let(:client) do
192
+ described_class.new(transport_class: OpenSearch::Transport::Transport::HTTP::Curb,
193
+ transport_options: { headers: { 'User-Agent' => 'testing' } })
194
+ end
195
+
196
+ it 'sets the specified User-Agent header' do
197
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to eq('testing')
198
+ end
199
+ end
200
+
201
+ context 'when a user-agent header is specified as a client option as lower-case' do
202
+
203
+ let(:client) do
204
+ described_class.new(transport_class: OpenSearch::Transport::Transport::HTTP::Curb,
205
+ transport_options: { headers: { 'user-agent' => 'testing' } })
206
+ end
207
+
208
+ it 'sets the specified User-Agent header' do
209
+ expect(client.transport.connections.first.connection.headers['User-Agent']).to eq('testing')
210
+ end
211
+ end
212
+
213
+ context 'when a Content-Type header is specified as client option' do
214
+
215
+ let(:client) do
216
+ described_class.new(transport_class: OpenSearch::Transport::Transport::HTTP::Curb,
217
+ transport_options: { headers: { 'Content-Type' => 'testing' } })
218
+ end
219
+
220
+ it 'sets the specified Content-Type header' do
221
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('testing')
222
+ end
223
+ end
224
+
225
+ context 'when a content-type header is specified as client option in lower-case' do
226
+
227
+ let(:client) do
228
+ described_class.new(transport_class: OpenSearch::Transport::Transport::HTTP::Curb,
229
+ transport_options: { headers: { 'content-type' => 'testing' } })
230
+ end
231
+
232
+ it 'sets the specified Content-Type header' do
233
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('testing')
234
+ end
235
+ end
236
+ end
237
+
238
+ describe 'adapter' do
239
+ context 'when no adapter is specified' do
240
+ fork do
241
+ let(:client) { described_class.new }
242
+ let(:adapter) { client.transport.connections.all.first.connection.builder.adapter }
243
+
244
+ it 'uses Faraday NetHttp' do
245
+ expect(adapter).to eq Faraday::Adapter::NetHttp
246
+ end
247
+ end unless jruby?
248
+ end
249
+
250
+ context 'when the adapter is patron' do
251
+ let(:adapter) do
252
+ client.transport.connections.all.first.connection.builder.adapter
253
+ end
254
+
255
+ let(:client) do
256
+ described_class.new(adapter: :patron)
257
+ end
258
+
259
+ it 'uses Faraday with the adapter' do
260
+ expect(adapter).to eq Faraday::Adapter::Patron
261
+ end
262
+ end
263
+
264
+ context 'when the adapter is typhoeus' do
265
+ let(:adapter) do
266
+ client.transport.connections.all.first.connection.builder.adapter
267
+ end
268
+
269
+ let(:client) do
270
+ described_class.new(adapter: :typhoeus)
271
+ end
272
+
273
+ it 'uses Faraday with the adapter' do
274
+ expect(adapter).to eq Faraday::Adapter::Typhoeus
275
+ end
276
+ end unless jruby?
277
+
278
+ context 'when the adapter is specified as a string key' do
279
+ let(:adapter) do
280
+ client.transport.connections.all.first.connection.builder.adapter
281
+ end
282
+
283
+ let(:client) do
284
+ described_class.new(adapter: :patron)
285
+ end
286
+
287
+ it 'uses Faraday with the adapter' do
288
+ expect(adapter).to eq Faraday::Adapter::Patron
289
+ end
290
+ end
291
+
292
+ context 'when the adapter can be detected', unless: jruby? do
293
+
294
+ around do |example|
295
+ require 'patron'; load 'patron.rb'
296
+ example.run
297
+ end
298
+
299
+ let(:adapter) do
300
+ client.transport.connections.all.first.connection.builder.adapter
301
+ end
302
+
303
+ it 'uses the detected adapter' do
304
+ expect(adapter).to eq Faraday::Adapter::Patron
305
+ end
306
+ end
307
+
308
+ context 'when the Faraday adapter is configured' do
309
+
310
+ let(:client) do
311
+ described_class.new do |faraday|
312
+ faraday.adapter :patron
313
+ faraday.response :logger
314
+ end
315
+ end
316
+
317
+ let(:adapter) do
318
+ client.transport.connections.all.first.connection.builder.adapter
319
+ end
320
+
321
+ let(:handlers) do
322
+ client.transport.connections.all.first.connection.builder.handlers
323
+ end
324
+
325
+ it 'sets the adapter' do
326
+ expect(adapter).to eq Faraday::Adapter::Patron
327
+ end
328
+
329
+ it 'sets the logger' do
330
+ expect(handlers).to include(Faraday::Response::Logger)
331
+ end
332
+ end
333
+ end
334
+
335
+ shared_examples_for 'a client that extracts hosts' do
336
+
337
+ context 'when the host is a String' do
338
+
339
+ context 'when there is a protocol specified' do
340
+
341
+ context 'when credentials are specified \'http://USERNAME:PASSWORD@myhost:8080\'' do
342
+
343
+ let(:host) do
344
+ 'http://USERNAME:PASSWORD@myhost:8080'
345
+ end
346
+
347
+ it 'extracts the credentials' do
348
+ expect(hosts[0][:user]).to eq('USERNAME')
349
+ expect(hosts[0][:password]).to eq('PASSWORD')
350
+ end
351
+
352
+ it 'extracts the host' do
353
+ expect(hosts[0][:host]).to eq('myhost')
354
+ end
355
+
356
+ it 'extracts the port' do
357
+ expect(hosts[0][:port]).to be(8080)
358
+ end
359
+ end
360
+
361
+ context 'when there is a trailing slash \'http://myhost/\'' do
362
+
363
+ let(:host) do
364
+ 'http://myhost/'
365
+ end
366
+
367
+ it 'extracts the host' do
368
+ expect(hosts[0][:host]).to eq('myhost')
369
+ expect(hosts[0][:scheme]).to eq('http')
370
+ expect(hosts[0][:path]).to eq('')
371
+ end
372
+
373
+ it 'extracts the scheme' do
374
+ expect(hosts[0][:scheme]).to eq('http')
375
+ end
376
+
377
+ it 'extracts the path' do
378
+ expect(hosts[0][:path]).to eq('')
379
+ end
380
+ end
381
+
382
+ context 'when there is a trailing slash with a path \'http://myhost/foo/bar/\'' do
383
+
384
+ let(:host) do
385
+ 'http://myhost/foo/bar/'
386
+ end
387
+
388
+ it 'extracts the host' do
389
+ expect(hosts[0][:host]).to eq('myhost')
390
+ expect(hosts[0][:scheme]).to eq('http')
391
+ expect(hosts[0][:path]).to eq('/foo/bar')
392
+ end
393
+ end
394
+
395
+ context 'when the protocol is http' do
396
+
397
+ context 'when there is no port specified \'http://myhost\'' do
398
+
399
+ let(:host) do
400
+ 'http://myhost'
401
+ end
402
+
403
+ it 'extracts the host' do
404
+ expect(hosts[0][:host]).to eq('myhost')
405
+ end
406
+
407
+ it 'extracts the protocol' do
408
+ expect(hosts[0][:protocol]).to eq('http')
409
+ end
410
+
411
+ it 'defaults to port 9200' do
412
+ expect(hosts[0][:port]).to be(9200)
413
+ end
414
+ end
415
+
416
+ context 'when there is a port specified \'http://myhost:7101\'' do
417
+
418
+ let(:host) do
419
+ 'http://myhost:7101'
420
+ end
421
+
422
+ it 'extracts the host' do
423
+ expect(hosts[0][:host]).to eq('myhost')
424
+ end
425
+
426
+ it 'extracts the protocol' do
427
+ expect(hosts[0][:protocol]).to eq('http')
428
+ end
429
+
430
+ it 'extracts the port' do
431
+ expect(hosts[0][:port]).to be(7101)
432
+ end
433
+
434
+ context 'when there is a path specified \'http://myhost:7101/api\'' do
435
+
436
+ let(:host) do
437
+ 'http://myhost:7101/api'
438
+ end
439
+
440
+ it 'sets the path' do
441
+ expect(hosts[0][:host]).to eq('myhost')
442
+ expect(hosts[0][:protocol]).to eq('http')
443
+ expect(hosts[0][:path]).to eq('/api')
444
+ expect(hosts[0][:port]).to be(7101)
445
+ end
446
+
447
+ it 'extracts the host' do
448
+ expect(hosts[0][:host]).to eq('myhost')
449
+ end
450
+
451
+ it 'extracts the protocol' do
452
+ expect(hosts[0][:protocol]).to eq('http')
453
+ end
454
+
455
+ it 'extracts the port' do
456
+ expect(hosts[0][:port]).to be(7101)
457
+ end
458
+
459
+ it 'extracts the path' do
460
+ expect(hosts[0][:path]).to eq('/api')
461
+ end
462
+ end
463
+ end
464
+ end
465
+
466
+ context 'when the protocol is https' do
467
+
468
+ context 'when there is no port specified \'https://myhost\'' do
469
+
470
+ let(:host) do
471
+ 'https://myhost'
472
+ end
473
+
474
+ it 'extracts the host' do
475
+ expect(hosts[0][:host]).to eq('myhost')
476
+ end
477
+
478
+ it 'extracts the protocol' do
479
+ expect(hosts[0][:protocol]).to eq('https')
480
+ end
481
+
482
+ it 'defaults to port 443' do
483
+ expect(hosts[0][:port]).to be(443)
484
+ end
485
+ end
486
+
487
+ context 'when there is a port specified \'https://myhost:7101\'' do
488
+
489
+ let(:host) do
490
+ 'https://myhost:7101'
491
+ end
492
+
493
+ it 'extracts the host' do
494
+ expect(hosts[0][:host]).to eq('myhost')
495
+ end
496
+
497
+ it 'extracts the protocol' do
498
+ expect(hosts[0][:protocol]).to eq('https')
499
+ end
500
+
501
+ it 'extracts the port' do
502
+ expect(hosts[0][:port]).to be(7101)
503
+ end
504
+
505
+ context 'when there is a path specified \'https://myhost:7101/api\'' do
506
+
507
+ let(:host) do
508
+ 'https://myhost:7101/api'
509
+ end
510
+
511
+ it 'extracts the host' do
512
+ expect(hosts[0][:host]).to eq('myhost')
513
+ end
514
+
515
+ it 'extracts the protocol' do
516
+ expect(hosts[0][:protocol]).to eq('https')
517
+ end
518
+
519
+ it 'extracts the port' do
520
+ expect(hosts[0][:port]).to be(7101)
521
+ end
522
+
523
+ it 'extracts the path' do
524
+ expect(hosts[0][:path]).to eq('/api')
525
+ end
526
+ end
527
+ end
528
+
529
+ context 'when IPv6 format is used' do
530
+
531
+ around do |example|
532
+ original_setting = Faraday.ignore_env_proxy
533
+ Faraday.ignore_env_proxy = true
534
+ example.run
535
+ Faraday.ignore_env_proxy = original_setting
536
+ end
537
+
538
+ let(:host) do
539
+ 'https://[2090:db8:85a3:9811::1f]:8080'
540
+ end
541
+
542
+ it 'extracts the host' do
543
+ expect(hosts[0][:host]).to eq('[2090:db8:85a3:9811::1f]')
544
+ end
545
+
546
+ it 'extracts the protocol' do
547
+ expect(hosts[0][:protocol]).to eq('https')
548
+ end
549
+
550
+ it 'extracts the port' do
551
+ expect(hosts[0][:port]).to be(8080)
552
+ end
553
+
554
+ it 'creates the correct full url' do
555
+ expect(client.transport.__full_url(client.transport.hosts[0])).to eq('https://[2090:db8:85a3:9811::1f]:8080')
556
+ end
557
+ end
558
+ end
559
+ end
560
+
561
+ context 'when no protocol is specified \'myhost\'' do
562
+
563
+ let(:host) do
564
+ 'myhost'
565
+ end
566
+
567
+ it 'defaults to http' do
568
+ expect(hosts[0][:host]).to eq('myhost')
569
+ expect(hosts[0][:protocol]).to eq('http')
570
+ end
571
+
572
+ it 'uses port 9200' do
573
+ expect(hosts[0][:port]).to be(9200)
574
+ end
575
+ end
576
+ end
577
+
578
+ context 'when the host is a Hash' do
579
+
580
+ let(:host) do
581
+ { :host => 'myhost', :scheme => 'https' }
582
+ end
583
+
584
+ it 'extracts the host' do
585
+ expect(hosts[0][:host]).to eq('myhost')
586
+ end
587
+
588
+ it 'extracts the protocol' do
589
+ expect(hosts[0][:protocol]).to eq('https')
590
+ end
591
+
592
+ it 'extracts the port' do
593
+ expect(hosts[0][:port]).to be(9200)
594
+ end
595
+
596
+ context 'when IPv6 format is used' do
597
+
598
+ around do |example|
599
+ original_setting = Faraday.ignore_env_proxy
600
+ Faraday.ignore_env_proxy = true
601
+ example.run
602
+ Faraday.ignore_env_proxy = original_setting
603
+ end
604
+
605
+ let(:host) do
606
+ { host: '[2090:db8:85a3:9811::1f]', scheme: 'https', port: '443' }
607
+ end
608
+
609
+ it 'extracts the host' do
610
+ expect(hosts[0][:host]).to eq('[2090:db8:85a3:9811::1f]')
611
+ expect(hosts[0][:scheme]).to eq('https')
612
+ expect(hosts[0][:port]).to be(443)
613
+ end
614
+
615
+ it 'creates the correct full url' do
616
+ expect(client.transport.__full_url(client.transport.hosts[0])).to eq('https://[2090:db8:85a3:9811::1f]:443')
617
+ end
618
+ end
619
+
620
+ context 'when the host is localhost as a IPv6 address' do
621
+
622
+ around do |example|
623
+ original_setting = Faraday.ignore_env_proxy
624
+ Faraday.ignore_env_proxy = true
625
+ example.run
626
+ Faraday.ignore_env_proxy = original_setting
627
+ end
628
+
629
+ let(:host) do
630
+ { host: '[::1]' }
631
+ end
632
+
633
+ it 'extracts the host' do
634
+ expect(hosts[0][:host]).to eq('[::1]')
635
+ expect(hosts[0][:port]).to be(9200)
636
+ end
637
+
638
+ it 'creates the correct full url' do
639
+ expect(client.transport.__full_url(client.transport.hosts[0])).to eq('http://[::1]:9200')
640
+ end
641
+ end
642
+
643
+ context 'when the port is specified as a String' do
644
+
645
+ let(:host) do
646
+ { host: 'myhost', scheme: 'https', port: '443' }
647
+ end
648
+
649
+ it 'extracts the host' do
650
+ expect(hosts[0][:host]).to eq('myhost')
651
+ end
652
+
653
+ it 'extracts the protocol' do
654
+ expect(hosts[0][:scheme]).to eq('https')
655
+ end
656
+
657
+ it 'converts the port to an integer' do
658
+ expect(hosts[0][:port]).to be(443)
659
+ end
660
+ end
661
+
662
+ context 'when the port is specified as an Integer' do
663
+
664
+ let(:host) do
665
+ { host: 'myhost', scheme: 'https', port: 443 }
666
+ end
667
+
668
+ it 'extracts the host' do
669
+ expect(hosts[0][:host]).to eq('myhost')
670
+ end
671
+
672
+ it 'extracts the protocol' do
673
+ expect(hosts[0][:scheme]).to eq('https')
674
+ end
675
+
676
+ it 'extracts port as an integer' do
677
+ expect(hosts[0][:port]).to be(443)
678
+ end
679
+ end
680
+ end
681
+
682
+ context 'when the hosts are a Hashie:Mash' do
683
+
684
+ let(:host) do
685
+ Hashie::Mash.new(host: 'myhost', scheme: 'https')
686
+ end
687
+
688
+ it 'extracts the host' do
689
+ expect(hosts[0][:host]).to eq('myhost')
690
+ end
691
+
692
+ it 'extracts the protocol' do
693
+ expect(hosts[0][:scheme]).to eq('https')
694
+ end
695
+
696
+ it 'converts the port to an integer' do
697
+ expect(hosts[0][:port]).to be(9200)
698
+ end
699
+
700
+ context 'when the port is specified as a String' do
701
+
702
+ let(:host) do
703
+ Hashie::Mash.new(host: 'myhost', scheme: 'https', port: '443')
704
+ end
705
+
706
+ it 'extracts the host' do
707
+ expect(hosts[0][:host]).to eq('myhost')
708
+ end
709
+
710
+ it 'extracts the protocol' do
711
+ expect(hosts[0][:scheme]).to eq('https')
712
+ end
713
+
714
+ it 'converts the port to an integer' do
715
+ expect(hosts[0][:port]).to be(443)
716
+ end
717
+ end
718
+
719
+ context 'when the port is specified as an Integer' do
720
+
721
+ let(:host) do
722
+ Hashie::Mash.new(host: 'myhost', scheme: 'https', port: 443)
723
+ end
724
+
725
+ it 'extracts the host' do
726
+ expect(hosts[0][:host]).to eq('myhost')
727
+ end
728
+
729
+ it 'extracts the protocol' do
730
+ expect(hosts[0][:scheme]).to eq('https')
731
+ end
732
+
733
+ it 'extracts port as an integer' do
734
+ expect(hosts[0][:port]).to be(443)
735
+ end
736
+ end
737
+ end
738
+
739
+ context 'when the hosts are an array' do
740
+
741
+ context 'when there is one host' do
742
+
743
+ let(:host) do
744
+ ['myhost']
745
+ end
746
+
747
+ it 'extracts the host' do
748
+ expect(hosts[0][:host]).to eq('myhost')
749
+ end
750
+
751
+ it 'extracts the protocol' do
752
+ expect(hosts[0][:protocol]).to eq('http')
753
+ end
754
+
755
+ it 'defaults to port 9200' do
756
+ expect(hosts[0][:port]).to be(9200)
757
+ end
758
+ end
759
+
760
+ context 'when there is one host with a protocol and no port' do
761
+
762
+ let(:host) do
763
+ ['http://myhost']
764
+ end
765
+
766
+ it 'extracts the host' do
767
+ expect(hosts[0][:host]).to eq('myhost')
768
+ end
769
+
770
+ it 'extracts the protocol' do
771
+ expect(hosts[0][:scheme]).to eq('http')
772
+ end
773
+
774
+ it 'defaults to port 9200' do
775
+ expect(hosts[0][:port]).to be(9200)
776
+ end
777
+ end
778
+
779
+ context 'when there is one host with a protocol and the default http port explicitly provided' do
780
+ let(:host) do
781
+ ['http://myhost:80']
782
+ end
783
+
784
+ it 'respects the explicit port' do
785
+ expect(hosts[0][:port]).to be(80)
786
+ end
787
+ end
788
+
789
+ context 'when there is one host with a protocol and the default https port explicitly provided' do
790
+ let(:host) do
791
+ ['https://myhost:443']
792
+ end
793
+
794
+ it 'respects the explicit port' do
795
+ expect(hosts[0][:port]).to be(443)
796
+ end
797
+ end
798
+
799
+ context 'when there is one host with a protocol and no port' do
800
+
801
+ let(:host) do
802
+ ['https://myhost']
803
+ end
804
+
805
+ it 'extracts the host' do
806
+ expect(hosts[0][:host]).to eq('myhost')
807
+ end
808
+
809
+ it 'extracts the protocol' do
810
+ expect(hosts[0][:scheme]).to eq('https')
811
+ end
812
+
813
+ it 'defaults to port 443' do
814
+ expect(hosts[0][:port]).to be(443)
815
+ end
816
+ end
817
+
818
+ context 'when there is one host with a protocol, path, and no port' do
819
+
820
+ let(:host) do
821
+ ['http://myhost/foo/bar']
822
+ end
823
+
824
+ it 'extracts the host' do
825
+ expect(hosts[0][:host]).to eq('myhost')
826
+ end
827
+
828
+ it 'extracts the protocol' do
829
+ expect(hosts[0][:scheme]).to eq('http')
830
+ end
831
+
832
+ it 'defaults to port 9200' do
833
+ expect(hosts[0][:port]).to be(9200)
834
+ end
835
+
836
+ it 'extracts the path' do
837
+ expect(hosts[0][:path]).to eq('/foo/bar')
838
+ end
839
+ end
840
+
841
+ context 'when there is more than one host' do
842
+
843
+ let(:host) do
844
+ ['host1', 'host2']
845
+ end
846
+
847
+ it 'extracts the hosts' do
848
+ expect(hosts[0][:host]).to eq('host1')
849
+ expect(hosts[0][:protocol]).to eq('http')
850
+ expect(hosts[0][:port]).to be(9200)
851
+ expect(hosts[1][:host]).to eq('host2')
852
+ expect(hosts[1][:protocol]).to eq('http')
853
+ expect(hosts[1][:port]).to be(9200)
854
+ end
855
+ end
856
+
857
+ context 'when ports are also specified' do
858
+
859
+ let(:host) do
860
+ ['host1:1000', 'host2:2000']
861
+ end
862
+
863
+ it 'extracts the hosts' do
864
+ expect(hosts[0][:host]).to eq('host1')
865
+ expect(hosts[0][:protocol]).to eq('http')
866
+ expect(hosts[0][:port]).to be(1000)
867
+ expect(hosts[1][:host]).to eq('host2')
868
+ expect(hosts[1][:protocol]).to eq('http')
869
+ expect(hosts[1][:port]).to be(2000)
870
+ end
871
+ end
872
+ end
873
+
874
+ context 'when the hosts is an instance of URI' do
875
+
876
+ let(:host) do
877
+ URI.parse('https://USERNAME:PASSWORD@myhost:4430')
878
+ end
879
+
880
+ it 'extracts the host' do
881
+ expect(hosts[0][:host]).to eq('myhost')
882
+ expect(hosts[0][:scheme]).to eq('https')
883
+ expect(hosts[0][:port]).to be(4430)
884
+ expect(hosts[0][:user]).to eq('USERNAME')
885
+ expect(hosts[0][:password]).to eq('PASSWORD')
886
+ end
887
+ end
888
+
889
+ context 'when the hosts is invalid' do
890
+
891
+ let(:host) do
892
+ 123
893
+ end
894
+
895
+ it 'extracts the host' do
896
+ expect {
897
+ hosts
898
+ }.to raise_exception(ArgumentError)
899
+ end
900
+ end
901
+ end
902
+
903
+ context 'when hosts are specified with the \'host\' key' do
904
+
905
+ let(:client) do
906
+ described_class.new(host: ['host1', 'host2', 'host3', 'host4'], randomize_hosts: true)
907
+ end
908
+
909
+ let(:hosts) do
910
+ client.transport.hosts
911
+ end
912
+
913
+ it 'sets the hosts in random order' do
914
+ expect(hosts.all? { |host| client.transport.hosts.include?(host) }).to be(true)
915
+ end
916
+ end
917
+
918
+ context 'when hosts are specified with the \'host\' key as a String' do
919
+
920
+ let(:client) do
921
+ described_class.new('host' => ['host1', 'host2', 'host3', 'host4'], 'randomize_hosts' => true)
922
+ end
923
+
924
+ let(:hosts) do
925
+ client.transport.hosts
926
+ end
927
+
928
+ it 'sets the hosts in random order' do
929
+ expect(hosts.all? { |host| client.transport.hosts.include?(host) }).to be(true)
930
+ end
931
+ end
932
+
933
+ context 'when hosts are specified with the \'hosts\' key' do
934
+
935
+ let(:client) do
936
+ described_class.new(hosts: host)
937
+ end
938
+
939
+ let(:hosts) do
940
+ client.transport.hosts
941
+ end
942
+
943
+ it_behaves_like 'a client that extracts hosts'
944
+ end
945
+
946
+ context 'when hosts are specified with the \'hosts\' key as a String' do
947
+
948
+ let(:client) do
949
+ described_class.new('hosts' => host)
950
+ end
951
+
952
+ let(:hosts) do
953
+ client.transport.hosts
954
+ end
955
+
956
+ it_behaves_like 'a client that extracts hosts'
957
+ end
958
+
959
+ context 'when hosts are specified with the \'url\' key' do
960
+
961
+ let(:client) do
962
+ described_class.new(url: host)
963
+ end
964
+
965
+ let(:hosts) do
966
+ client.transport.hosts
967
+ end
968
+
969
+ it_behaves_like 'a client that extracts hosts'
970
+ end
971
+
972
+ context 'when hosts are specified with the \'url\' key as a String' do
973
+
974
+ let(:client) do
975
+ described_class.new('url' => host)
976
+ end
977
+
978
+ let(:hosts) do
979
+ client.transport.hosts
980
+ end
981
+
982
+ it_behaves_like 'a client that extracts hosts'
983
+ end
984
+
985
+ context 'when hosts are specified with the \'urls\' key' do
986
+
987
+ let(:client) do
988
+ described_class.new(urls: host)
989
+ end
990
+
991
+ let(:hosts) do
992
+ client.transport.hosts
993
+ end
994
+
995
+ it_behaves_like 'a client that extracts hosts'
996
+ end
997
+
998
+ context 'when hosts are specified with the \'urls\' key as a String' do
999
+
1000
+ let(:client) do
1001
+ described_class.new('urls' => host)
1002
+ end
1003
+
1004
+ let(:hosts) do
1005
+ client.transport.hosts
1006
+ end
1007
+
1008
+ it_behaves_like 'a client that extracts hosts'
1009
+ end
1010
+
1011
+ context 'when the URL is set in the OPENSEARCH_URL environment variable' do
1012
+
1013
+ context 'when there is only one host specified' do
1014
+
1015
+ around do |example|
1016
+ before_url = ENV['OPENSEARCH_URL']
1017
+ ENV['OPENSEARCH_URL'] = 'example.com'
1018
+ example.run
1019
+ ENV['OPENSEARCH_URL'] = before_url
1020
+ end
1021
+
1022
+ it 'sets the host' do
1023
+ expect(client.transport.hosts[0][:host]).to eq('example.com')
1024
+ expect(client.transport.hosts.size).to eq(1)
1025
+ end
1026
+ end
1027
+
1028
+ context 'when mutliple hosts are specified as a comma-separated String list' do
1029
+
1030
+ around do |example|
1031
+ before_url = ENV['OPENSEARCH_URL']
1032
+ ENV['OPENSEARCH_URL'] = 'example.com, other.com'
1033
+ example.run
1034
+ ENV['OPENSEARCH_URL'] = before_url
1035
+ end
1036
+
1037
+ it 'sets the hosts' do
1038
+ expect(client.transport.hosts[0][:host]).to eq('example.com')
1039
+ expect(client.transport.hosts[1][:host]).to eq('other.com')
1040
+ expect(client.transport.hosts.size).to eq(2)
1041
+ end
1042
+ end
1043
+ end
1044
+
1045
+ context 'when options are defined' do
1046
+
1047
+ context 'when scheme is specified' do
1048
+
1049
+ let(:client) do
1050
+ described_class.new(scheme: 'https')
1051
+ end
1052
+
1053
+ it 'sets the scheme' do
1054
+ expect(client.transport.connections[0].full_url('')).to match(/https/)
1055
+ end
1056
+ end
1057
+
1058
+ context 'when scheme is specified as a String key' do
1059
+
1060
+ let(:client) do
1061
+ described_class.new('scheme' => 'https')
1062
+ end
1063
+
1064
+ it 'sets the scheme' do
1065
+ expect(client.transport.connections[0].full_url('')).to match(/https/)
1066
+ end
1067
+ end
1068
+
1069
+ context 'when user and password are specified' do
1070
+
1071
+ let(:client) do
1072
+ described_class.new(user: 'USERNAME', password: 'PASSWORD')
1073
+ end
1074
+
1075
+ it 'sets the user and password' do
1076
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
1077
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
1078
+ end
1079
+
1080
+ context 'when the connections are reloaded' do
1081
+
1082
+ before do
1083
+ allow(client.transport.sniffer).to receive(:hosts).and_return([{ host: 'foobar', port: 4567, id: 'foobar4567' }])
1084
+ client.transport.reload_connections!
1085
+ end
1086
+
1087
+ it 'sets keeps user and password' do
1088
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
1089
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
1090
+ expect(client.transport.connections[0].full_url('')).to match(/foobar/)
1091
+ end
1092
+ end
1093
+ end
1094
+
1095
+ context 'when user and password are specified as String keys' do
1096
+
1097
+ let(:client) do
1098
+ described_class.new('user' => 'USERNAME', 'password' => 'PASSWORD')
1099
+ end
1100
+
1101
+ it 'sets the user and password' do
1102
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
1103
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
1104
+ end
1105
+
1106
+ context 'when the connections are reloaded' do
1107
+
1108
+ before do
1109
+ allow(client.transport.sniffer).to receive(:hosts).and_return([{ host: 'foobar', port: 4567, id: 'foobar4567' }])
1110
+ client.transport.reload_connections!
1111
+ end
1112
+
1113
+ it 'sets keeps user and password' do
1114
+ expect(client.transport.connections[0].full_url('')).to match(/USERNAME/)
1115
+ expect(client.transport.connections[0].full_url('')).to match(/PASSWORD/)
1116
+ expect(client.transport.connections[0].full_url('')).to match(/foobar/)
1117
+ end
1118
+ end
1119
+ end
1120
+
1121
+ context 'when port is specified' do
1122
+
1123
+ let(:client) do
1124
+ described_class.new(host: 'node1', port: 1234)
1125
+ end
1126
+
1127
+ it 'sets the port' do
1128
+ expect(client.transport.connections[0].full_url('')).to match(/1234/)
1129
+ end
1130
+ end
1131
+
1132
+ context 'when the log option is true' do
1133
+
1134
+ let(:client) do
1135
+ described_class.new(log: true)
1136
+ end
1137
+
1138
+ it 'has a default logger for transport' do
1139
+ expect(client.transport.logger.info).to eq(described_class::DEFAULT_LOGGER.call.info)
1140
+ end
1141
+ end
1142
+
1143
+ context 'when the trace option is true' do
1144
+
1145
+ let(:client) do
1146
+ described_class.new(trace: true)
1147
+ end
1148
+
1149
+ it 'has a default logger for transport' do
1150
+ expect(client.transport.tracer.info).to eq(described_class::DEFAULT_TRACER.call.info)
1151
+ end
1152
+ end
1153
+
1154
+ context 'when a custom transport class is specified' do
1155
+
1156
+ let(:transport_class) do
1157
+ Class.new { def initialize(*); end }
1158
+ end
1159
+
1160
+ let(:client) do
1161
+ described_class.new(transport_class: transport_class)
1162
+ end
1163
+
1164
+ it 'allows the custom transport class to be defined' do
1165
+ expect(client.transport).to be_a(transport_class)
1166
+ end
1167
+ end
1168
+
1169
+ context 'when a custom transport instance is specified' do
1170
+
1171
+ let(:transport_instance) do
1172
+ Class.new { def initialize(*); end }.new
1173
+ end
1174
+
1175
+ let(:client) do
1176
+ described_class.new(transport: transport_instance)
1177
+ end
1178
+
1179
+ it 'allows the custom transport class to be defined' do
1180
+ expect(client.transport).to be(transport_instance)
1181
+ end
1182
+ end
1183
+
1184
+ context 'when \'transport_options\' are defined' do
1185
+
1186
+ let(:client) do
1187
+ described_class.new(transport_options: { request: { timeout: 1 } })
1188
+ end
1189
+
1190
+ it 'sets the options on the transport' do
1191
+ expect(client.transport.options[:transport_options][:request]).to eq(timeout: 1)
1192
+ end
1193
+ end
1194
+
1195
+ context 'when \'request_timeout\' is defined' do
1196
+
1197
+ let(:client) do
1198
+ described_class.new(request_timeout: 120)
1199
+ end
1200
+
1201
+ it 'sets the options on the transport' do
1202
+ expect(client.transport.options[:transport_options][:request]).to eq(timeout: 120)
1203
+ end
1204
+ end
1205
+
1206
+ context 'when \'request_timeout\' is defined as a String key' do
1207
+
1208
+ let(:client) do
1209
+ described_class.new('request_timeout' => 120)
1210
+ end
1211
+
1212
+ it 'sets the options on the transport' do
1213
+ expect(client.transport.options[:transport_options][:request]).to eq(timeout: 120)
1214
+ end
1215
+ end
1216
+ end
1217
+
1218
+ describe '#perform_request' do
1219
+
1220
+ let(:transport_instance) do
1221
+ Class.new { def initialize(*); end }.new
1222
+ end
1223
+
1224
+ let(:client) do
1225
+ described_class.new(transport: transport_instance)
1226
+ end
1227
+
1228
+ it 'delegates performing requests to the transport' do
1229
+ expect(transport_instance).to receive(:perform_request).and_return(true)
1230
+ expect(client.perform_request('GET', '/')).to be(true)
1231
+ end
1232
+
1233
+ context 'when the \'send_get_body_as\' option is specified' do
1234
+
1235
+ let(:client) do
1236
+ described_class.new(transport: transport_instance, :send_get_body_as => 'POST')
1237
+ end
1238
+
1239
+ before do
1240
+ expect(transport_instance).to receive(:perform_request).with('POST', '/', {},
1241
+ '{"foo":"bar"}',
1242
+ '{"Content-Type":"application/x-ndjson"}').and_return(true)
1243
+ end
1244
+
1245
+ let(:request) do
1246
+ client.perform_request('POST', '/', {}, '{"foo":"bar"}', '{"Content-Type":"application/x-ndjson"}')
1247
+ end
1248
+
1249
+ it 'sets the option' do
1250
+ expect(request).to be(true)
1251
+ end
1252
+ end
1253
+
1254
+ context 'when using the API Compatibility Header' do
1255
+ it 'sets the API compatibility headers' do
1256
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = 'true'
1257
+ client = described_class.new(host: hosts)
1258
+ headers = client.transport.connections.first.connection.headers
1259
+
1260
+ expect(headers['Content-Type']).to eq('application/vnd.opensearch+json; compatible-with=7')
1261
+ expect(headers['Accept']).to eq('application/vnd.opensearch+json;compatible-with=7')
1262
+
1263
+ response = client.perform_request('GET', '/')
1264
+ expect(response.headers['content-type']).to eq('application/json; charset=UTF-8')
1265
+
1266
+ ENV.delete('ELASTIC_CLIENT_APIVERSIONING')
1267
+ end
1268
+
1269
+ it 'does not use API compatibility headers' do
1270
+ val = ENV.delete('ELASTIC_CLIENT_APIVERSIONING')
1271
+ client = described_class.new(host: hosts)
1272
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
1273
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = val
1274
+ end
1275
+
1276
+ it 'does not use API compatibility headers when it is set to unsupported values' do
1277
+ val = ENV.delete('ELASTIC_CLIENT_APIVERSIONING')
1278
+
1279
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = 'test'
1280
+ client = described_class.new(host: hosts)
1281
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
1282
+
1283
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = 'false'
1284
+ client = described_class.new(host: hosts)
1285
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
1286
+
1287
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = '3'
1288
+ client = described_class.new(host: hosts)
1289
+ expect(client.transport.connections.first.connection.headers['Content-Type']).to eq('application/json')
1290
+ ENV['ELASTIC_CLIENT_APIVERSIONING'] = val
1291
+ end
1292
+ end
1293
+
1294
+ context 'when OpenSearch response includes a warning header' do
1295
+ let(:client) do
1296
+ OpenSearch::Transport::Client.new(hosts: hosts)
1297
+ end
1298
+
1299
+ let(:warning) { 'OpenSearch warning: "deprecation warning"' }
1300
+
1301
+ it 'prints a warning' do
1302
+ allow_any_instance_of(OpenSearch::Transport::Transport::Response).to receive(:headers) do
1303
+ { 'warning' => warning }
1304
+ end
1305
+
1306
+ begin
1307
+ stderr = $stderr
1308
+ fake_stderr = StringIO.new
1309
+ $stderr = fake_stderr
1310
+
1311
+ client.perform_request('GET', '/')
1312
+ fake_stderr.rewind
1313
+ expect(fake_stderr.string).to eq("warning: #{warning}\n")
1314
+ ensure
1315
+ $stderr = stderr
1316
+ end
1317
+ end
1318
+ end
1319
+
1320
+ context 'when a header is set on an endpoint request' do
1321
+ let(:client) { described_class.new(host: hosts) }
1322
+ let(:headers) { { 'user-agent' => 'my ruby app' } }
1323
+
1324
+ it 'performs the request with the header' do
1325
+ allow(client).to receive(:perform_request) { OpenStruct.new(body: '') }
1326
+ expect { client.perform_request('GET', '_search', {}, nil, headers) }.not_to raise_error
1327
+ expect(client).to have_received(:perform_request)
1328
+ .with('GET', '_search', {}, nil, headers)
1329
+ end
1330
+ end
1331
+
1332
+ context 'when a header is set on an endpoint request and on initialization' do
1333
+ let!(:client) do
1334
+ described_class.new(
1335
+ host: hosts,
1336
+ transport_options: { headers: instance_headers }
1337
+ )
1338
+ end
1339
+ let(:instance_headers) { { set_in_instantiation: 'header value' } }
1340
+ let(:param_headers) { {'user-agent' => 'My Ruby Tests', 'set-on-method-call' => 'header value'} }
1341
+
1342
+ it 'performs the request with the header' do
1343
+ expected_headers = client.transport.connections.connections.first.connection.headers.merge(param_headers)
1344
+
1345
+ expect_any_instance_of(Faraday::Connection)
1346
+ .to receive(:run_request)
1347
+ .with(:get, "http://#{hosts[0]}/_search", nil, expected_headers) { OpenStruct.new(body: '')}
1348
+
1349
+ client.perform_request('GET', '_search', {}, nil, param_headers)
1350
+ end
1351
+ end
1352
+ end
1353
+
1354
+ context 'when the client connects to OpenSearch' do
1355
+ let(:logger) do
1356
+ Logger.new(STDERR).tap do |logger|
1357
+ logger.formatter = proc do |severity, datetime, progname, msg|
1358
+ color = case severity
1359
+ when /INFO/ then :green
1360
+ when /ERROR|WARN|FATAL/ then :red
1361
+ when /DEBUG/ then :cyan
1362
+ else :white
1363
+ end
1364
+ ANSI.ansi(severity[0] + ' ', color, :faint) + ANSI.ansi(msg, :white, :faint) + "\n"
1365
+ end
1366
+ end unless ENV['QUIET']
1367
+ end
1368
+
1369
+ let(:port) do
1370
+ TEST_PORT
1371
+ end
1372
+
1373
+ let(:transport_options) do
1374
+ {}
1375
+ end
1376
+
1377
+ let(:options) do
1378
+ {}
1379
+ end
1380
+
1381
+ let(:client) do
1382
+ described_class.new({ host: hosts, logger: logger }.merge!(transport_options: transport_options).merge!(options))
1383
+ end
1384
+
1385
+ context 'when a request is made' do
1386
+ let!(:response) do
1387
+ client.perform_request('GET', '_cluster/health')
1388
+ end
1389
+
1390
+ it 'connects to the cluster' do
1391
+ expect(response.body['number_of_nodes']).to be >= (1)
1392
+ end
1393
+ end
1394
+
1395
+ describe '#initialize' do
1396
+ context 'when options are specified' do
1397
+ let(:transport_options) do
1398
+ { headers: { accept: 'application/yaml', content_type: 'application/yaml' } }
1399
+ end
1400
+
1401
+ let(:response) do
1402
+ client.perform_request('GET', '_cluster/health')
1403
+ end
1404
+
1405
+ it 'applies the options to the client' do
1406
+ expect(response.body).to match(/---\n/)
1407
+ expect(response.headers['content-type']).to eq('application/yaml')
1408
+ end
1409
+ end
1410
+
1411
+ context 'when a block is provided' do
1412
+ let(:client) do
1413
+ described_class.new(host: OPENSEARCH_HOSTS.first, logger: logger) do |client|
1414
+ client.headers['Accept'] = 'application/yaml'
1415
+ end
1416
+ end
1417
+
1418
+ let(:response) do
1419
+ client.perform_request('GET', '_cluster/health')
1420
+ end
1421
+
1422
+ it 'executes the block' do
1423
+ expect(response.body).to match(/---\n/)
1424
+ expect(response.headers['content-type']).to eq('application/yaml')
1425
+ end
1426
+
1427
+ context 'when the Faraday adapter is set in the block' do
1428
+ let(:client) do
1429
+ described_class.new(host: OPENSEARCH_HOSTS.first, logger: logger) do |client|
1430
+ client.adapter(:net_http_persistent)
1431
+ end
1432
+ end
1433
+
1434
+ let(:handler_name) do
1435
+ client.transport.connections.first.connection.builder.adapter.name
1436
+ end
1437
+
1438
+ let(:response) do
1439
+ client.perform_request('GET', '_cluster/health')
1440
+ end
1441
+
1442
+ it 'sets the adapter' do
1443
+ expect(handler_name).to eq('Faraday::Adapter::NetHttpPersistent')
1444
+ end
1445
+
1446
+ it 'uses the adapter to connect' do
1447
+ expect(response.status).to eq(200)
1448
+ end
1449
+ end
1450
+ end
1451
+ end
1452
+
1453
+ describe '#options' do
1454
+
1455
+ context 'when retry_on_failure is true' do
1456
+
1457
+ context 'when a node is unreachable' do
1458
+
1459
+ let(:hosts) do
1460
+ [OPENSEARCH_HOSTS.first, "foobar1", "foobar2"]
1461
+ end
1462
+
1463
+ let(:options) do
1464
+ { retry_on_failure: true }
1465
+ end
1466
+
1467
+ let(:responses) do
1468
+ 5.times.collect do
1469
+ client.perform_request('GET', '_nodes/_local')
1470
+ end
1471
+ end
1472
+
1473
+ it 'retries on failure' do
1474
+ expect(responses.all? { true }).to be(true)
1475
+ end
1476
+ end
1477
+ end
1478
+
1479
+ context 'when retry_on_failure is an integer' do
1480
+
1481
+ let(:hosts) do
1482
+ [OPENSEARCH_HOSTS.first, 'foobar1', 'foobar2', 'foobar3']
1483
+ end
1484
+
1485
+ let(:options) do
1486
+ { retry_on_failure: 1 }
1487
+ end
1488
+
1489
+ it 'retries only the specified number of times' do
1490
+ expect(client.perform_request('GET', '_nodes/_local'))
1491
+ expect {
1492
+ client.perform_request('GET', '_nodes/_local')
1493
+ }.to raise_exception(Faraday::ConnectionFailed)
1494
+ end
1495
+ end
1496
+
1497
+ context 'when reload_on_failure is true' do
1498
+
1499
+ let(:hosts) do
1500
+ [OPENSEARCH_HOSTS.first, 'foobar1', 'foobar2']
1501
+ end
1502
+
1503
+ let(:options) do
1504
+ { reload_on_failure: true }
1505
+ end
1506
+
1507
+ let(:responses) do
1508
+ 5.times.collect do
1509
+ client.perform_request('GET', '_nodes/_local')
1510
+ end
1511
+ end
1512
+
1513
+ it 'reloads the connections' do
1514
+ expect(client.transport.connections.size).to eq(3)
1515
+ expect(responses.all? { true }).to be(true)
1516
+ expect(client.transport.connections.size).to be >= (1)
1517
+ end
1518
+ end
1519
+
1520
+ context 'when retry_on_status is specified' do
1521
+
1522
+ let(:options) do
1523
+ { retry_on_status: 400 }
1524
+ end
1525
+
1526
+ let(:logger) do
1527
+ double('logger', :debug? => false, :warn? => true, :fatal? => false, :error? => false)
1528
+ end
1529
+
1530
+ before do
1531
+ expect(logger).to receive(:warn).exactly(4).times
1532
+ end
1533
+
1534
+ it 'retries when the status matches' do
1535
+ expect {
1536
+ client.perform_request('PUT', '_foobar')
1537
+ }.to raise_exception(OpenSearch::Transport::Transport::Errors::BadRequest)
1538
+ end
1539
+ end
1540
+
1541
+ context 'when the \'compression\' option is set to true' do
1542
+
1543
+ context 'when using Faraday as the transport' do
1544
+
1545
+ context 'when using the Net::HTTP adapter' do
1546
+
1547
+ let(:client) do
1548
+ described_class.new(hosts: OPENSEARCH_HOSTS, compression: true, adapter: :net_http)
1549
+ end
1550
+
1551
+ it 'compresses the request and decompresses the response' do
1552
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1553
+ end
1554
+
1555
+ it 'sets the Accept-Encoding header' do
1556
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1557
+ end
1558
+
1559
+ it 'preserves the other headers' do
1560
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1561
+ end
1562
+ end
1563
+
1564
+ context 'when using the HTTPClient adapter' do
1565
+
1566
+ let(:client) do
1567
+ described_class.new(hosts: OPENSEARCH_HOSTS, compression: true, adapter: :httpclient)
1568
+ end
1569
+
1570
+ it 'compresses the request and decompresses the response' do
1571
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1572
+ end
1573
+
1574
+ it 'sets the Accept-Encoding header' do
1575
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1576
+ end
1577
+
1578
+ it 'preserves the other headers' do
1579
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1580
+ end
1581
+ end
1582
+
1583
+ context 'when using the Patron adapter', unless: jruby? do
1584
+
1585
+ let(:client) do
1586
+ described_class.new(hosts: OPENSEARCH_HOSTS, compression: true, adapter: :patron)
1587
+ end
1588
+
1589
+ it 'compresses the request and decompresses the response' do
1590
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1591
+ end
1592
+
1593
+ it 'sets the Accept-Encoding header' do
1594
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1595
+ end
1596
+
1597
+ it 'preserves the other headers' do
1598
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1599
+ end
1600
+ end
1601
+
1602
+ context 'when using the Net::HTTP::Persistent adapter' do
1603
+
1604
+ let(:client) do
1605
+ described_class.new(hosts: OPENSEARCH_HOSTS, compression: true, adapter: :net_http_persistent)
1606
+ end
1607
+
1608
+ it 'compresses the request and decompresses the response' do
1609
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1610
+ end
1611
+
1612
+ it 'sets the Accept-Encoding header' do
1613
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1614
+ end
1615
+
1616
+ it 'preserves the other headers' do
1617
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1618
+ end
1619
+ end
1620
+
1621
+ context 'when using the Typhoeus adapter' do
1622
+
1623
+ let(:client) do
1624
+ described_class.new(hosts: OPENSEARCH_HOSTS, compression: true, adapter: :typhoeus)
1625
+ end
1626
+
1627
+ it 'compresses the request and decompresses the response' do
1628
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1629
+ end
1630
+
1631
+ it 'sets the Accept-Encoding header' do
1632
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1633
+ end
1634
+
1635
+ it 'preserves the other headers' do
1636
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1637
+ end
1638
+ end unless jruby?
1639
+ end
1640
+ end
1641
+
1642
+ context 'when using Curb as the transport', unless: jruby? do
1643
+ let(:client) do
1644
+ described_class.new(
1645
+ hosts: OPENSEARCH_HOSTS,
1646
+ compression: true,
1647
+ transport_class: OpenSearch::Transport::Transport::HTTP::Curb
1648
+ )
1649
+ end
1650
+
1651
+ it 'compresses the request and decompresses the response' do
1652
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1653
+ end
1654
+
1655
+ it 'sets the Accept-Encoding header' do
1656
+ expect(client.transport.connections[0].connection.headers['Accept-Encoding'])
1657
+ end
1658
+
1659
+ it 'preserves the other headers' do
1660
+ expect(client.transport.connections[0].connection.headers['User-Agent'])
1661
+ end
1662
+ end
1663
+
1664
+ context 'when using Manticore as the transport', if: jruby? do
1665
+ let(:client) do
1666
+ described_class.new(hosts: OPENSEARCH_HOSTS,
1667
+ compression: true,
1668
+ transport_class: OpenSearch::Transport::Transport::HTTP::Manticore)
1669
+ end
1670
+
1671
+ it 'compresses the request and decompresses the response' do
1672
+ expect(client.perform_request('GET', '/').body).to be_a(Hash)
1673
+ end
1674
+ end
1675
+ end
1676
+
1677
+ describe '#perform_request' do
1678
+ context 'when a request is made' do
1679
+ before do
1680
+ client.perform_request('DELETE', '_all')
1681
+ client.perform_request('DELETE', 'myindex') rescue
1682
+ client.perform_request('PUT', 'myindex', {}, { settings: { number_of_shards: 2, number_of_replicas: 0 } })
1683
+ client.perform_request('PUT', 'myindex/mydoc/1', { routing: 'XYZ', timeout: '1s' }, { foo: 'bar' })
1684
+ client.perform_request('GET', '_cluster/health?wait_for_status=green&timeout=2s', {})
1685
+ end
1686
+
1687
+ let(:response) do
1688
+ client.perform_request('GET', 'myindex/mydoc/1?routing=XYZ')
1689
+ end
1690
+
1691
+ it 'handles paths and URL paramters' do
1692
+ expect(response.status).to eq(200)
1693
+ end
1694
+
1695
+ it 'returns response body' do
1696
+ expect(response.body['_source']).to eq('foo' => 'bar')
1697
+ end
1698
+ end
1699
+
1700
+ context 'when an invalid url is specified' do
1701
+ it 'raises an exception' do
1702
+ expect {
1703
+ client.perform_request('GET', 'myindex/mydoc/1?routing=FOOBARBAZ')
1704
+ }.to raise_exception(OpenSearch::Transport::Transport::Errors::NotFound)
1705
+ end
1706
+ end
1707
+
1708
+ context 'when the \'ignore\' parameter is specified' do
1709
+ let(:response) do
1710
+ client.perform_request('PUT', '_foobar', ignore: 400)
1711
+ end
1712
+
1713
+ it 'exposes the status in the response' do
1714
+ expect(response.status).to eq(400)
1715
+ end
1716
+
1717
+ it 'exposes the body of the response' do
1718
+ expect(response.body).to be_a(Hash)
1719
+ expect(response.body.inspect).to match(/invalid_index_name_exception/)
1720
+ end
1721
+ end
1722
+
1723
+ context 'when request headers are specified' do
1724
+
1725
+ let(:response) do
1726
+ client.perform_request('GET', '/', {}, nil, { 'Content-Type' => 'application/yaml' })
1727
+ end
1728
+
1729
+ it 'passes them to the transport' do
1730
+ expect(response.body).to match(/---/)
1731
+ end
1732
+ end
1733
+
1734
+ describe 'selector' do
1735
+
1736
+ context 'when the round-robin selector is used' do
1737
+
1738
+ let(:nodes) do
1739
+ 3.times.collect do
1740
+ client.perform_request('GET', '_nodes/_local').body['nodes'].to_a[0][1]['name']
1741
+ end
1742
+ end
1743
+
1744
+ let(:node_names) do
1745
+ client.perform_request('GET', '_nodes/stats').body('nodes').collect do |name, stats|
1746
+ stats['name']
1747
+ end
1748
+ end
1749
+
1750
+ let(:expected_names) do
1751
+ 3.times.collect do |i|
1752
+ node_names[i % node_names.size]
1753
+ end
1754
+ end
1755
+
1756
+ # it 'rotates nodes' do
1757
+ # pending 'Better way to detect rotating nodes'
1758
+ # expect(nodes).to eq(expected_names)
1759
+ # end
1760
+ end
1761
+ end
1762
+
1763
+ context 'when patron is used as an adapter', unless: jruby? do
1764
+ before do
1765
+ require 'patron'
1766
+ end
1767
+
1768
+ let(:options) do
1769
+ { adapter: :patron }
1770
+ end
1771
+
1772
+ let(:adapter) do
1773
+ client.transport.connections.first.connection.builder.adapter
1774
+ end
1775
+
1776
+ it 'uses the patron connection handler' do
1777
+ expect(adapter).to eq('Faraday::Adapter::Patron')
1778
+ end
1779
+
1780
+ it 'keeps connections open' do
1781
+ response = client.perform_request('GET', '_nodes/stats/http')
1782
+ connections_before = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1783
+ client.transport.reload_connections!
1784
+ response = client.perform_request('GET', '_nodes/stats/http')
1785
+ connections_after = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1786
+ expect(connections_after).to be >= (connections_before)
1787
+ end
1788
+ end
1789
+
1790
+ context 'when typhoeus is used as an adapter', unless: jruby? do
1791
+ before do
1792
+ require 'typhoeus'
1793
+ end
1794
+
1795
+ let(:options) do
1796
+ { adapter: :typhoeus }
1797
+ end
1798
+
1799
+ let(:adapter) do
1800
+ client.transport.connections.first.connection.builder.adapter
1801
+ end
1802
+
1803
+ it 'uses the patron connection handler' do
1804
+ expect(adapter).to eq('Faraday::Adapter::Typhoeus')
1805
+ end
1806
+
1807
+ it 'keeps connections open' do
1808
+ response = client.perform_request('GET', '_nodes/stats/http')
1809
+ connections_before = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1810
+ client.transport.reload_connections!
1811
+ response = client.perform_request('GET', '_nodes/stats/http')
1812
+ connections_after = response.body['nodes'].values.find { |n| n['name'] == node_names.first }['http']['total_opened']
1813
+ expect(connections_after).to be >= (connections_before)
1814
+ end
1815
+ end
1816
+ end
1817
+ end
1818
+ end