elastictastic 0.5.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 (57) hide show
  1. data/LICENSE +19 -0
  2. data/README.md +326 -0
  3. data/lib/elastictastic/association.rb +21 -0
  4. data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
  5. data/lib/elastictastic/callbacks.rb +30 -0
  6. data/lib/elastictastic/child_collection_proxy.rb +56 -0
  7. data/lib/elastictastic/client.rb +101 -0
  8. data/lib/elastictastic/configuration.rb +35 -0
  9. data/lib/elastictastic/dirty.rb +130 -0
  10. data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
  11. data/lib/elastictastic/document.rb +98 -0
  12. data/lib/elastictastic/errors.rb +7 -0
  13. data/lib/elastictastic/field.rb +38 -0
  14. data/lib/elastictastic/index.rb +19 -0
  15. data/lib/elastictastic/mass_assignment_security.rb +15 -0
  16. data/lib/elastictastic/middleware.rb +119 -0
  17. data/lib/elastictastic/nested_document.rb +29 -0
  18. data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
  19. data/lib/elastictastic/observer.rb +3 -0
  20. data/lib/elastictastic/observing.rb +21 -0
  21. data/lib/elastictastic/parent_child.rb +115 -0
  22. data/lib/elastictastic/persistence.rb +67 -0
  23. data/lib/elastictastic/properties.rb +236 -0
  24. data/lib/elastictastic/railtie.rb +35 -0
  25. data/lib/elastictastic/resource.rb +4 -0
  26. data/lib/elastictastic/scope.rb +283 -0
  27. data/lib/elastictastic/scope_builder.rb +32 -0
  28. data/lib/elastictastic/scoped.rb +20 -0
  29. data/lib/elastictastic/search.rb +180 -0
  30. data/lib/elastictastic/server_error.rb +15 -0
  31. data/lib/elastictastic/test_helpers.rb +172 -0
  32. data/lib/elastictastic/util.rb +63 -0
  33. data/lib/elastictastic/validations.rb +45 -0
  34. data/lib/elastictastic/version.rb +3 -0
  35. data/lib/elastictastic.rb +82 -0
  36. data/spec/environment.rb +6 -0
  37. data/spec/examples/active_model_lint_spec.rb +20 -0
  38. data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
  39. data/spec/examples/callbacks_spec.rb +96 -0
  40. data/spec/examples/dirty_spec.rb +238 -0
  41. data/spec/examples/document_spec.rb +600 -0
  42. data/spec/examples/mass_assignment_security_spec.rb +13 -0
  43. data/spec/examples/middleware_spec.rb +92 -0
  44. data/spec/examples/observing_spec.rb +141 -0
  45. data/spec/examples/parent_child_spec.rb +308 -0
  46. data/spec/examples/properties_spec.rb +92 -0
  47. data/spec/examples/scope_spec.rb +491 -0
  48. data/spec/examples/search_spec.rb +382 -0
  49. data/spec/examples/spec_helper.rb +15 -0
  50. data/spec/examples/validation_spec.rb +65 -0
  51. data/spec/models/author.rb +9 -0
  52. data/spec/models/blog.rb +5 -0
  53. data/spec/models/comment.rb +5 -0
  54. data/spec/models/post.rb +41 -0
  55. data/spec/models/post_observer.rb +11 -0
  56. data/spec/support/fakeweb_request_history.rb +13 -0
  57. metadata +227 -0
@@ -0,0 +1,600 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Document do
4
+ include Elastictastic::TestHelpers
5
+
6
+ let(:last_request) { FakeWeb.last_request }
7
+ let(:last_request_body) { JSON.parse(last_request.body) }
8
+
9
+ describe '#save' do
10
+ context 'new object' do
11
+ let!(:id) { stub_elasticsearch_create('default', 'post') }
12
+ let(:post) { Post.new }
13
+
14
+ before do
15
+ post.title = 'Hot Pasta'
16
+ post.save
17
+ end
18
+
19
+ it 'should send POST request' do
20
+ last_request.method.should == 'POST'
21
+ end
22
+
23
+ it 'should send to index/type path' do
24
+ last_request.path.should == '/default/post'
25
+ end
26
+
27
+ it 'should send document in the body' do
28
+ last_request.body.should == post.elasticsearch_doc.to_json
29
+ end
30
+
31
+ it 'should populate ID of model object' do
32
+ post.id.should == id
33
+ end
34
+
35
+ it 'should mark object as persisted' do
36
+ post.should be_persisted
37
+ end
38
+ end # context 'new object'
39
+
40
+ context 'new object with ID' do
41
+ let(:post) { Post.new.tap { |post| post.id = '123' }}
42
+
43
+ context 'with unique id' do
44
+ before do
45
+ stub_elasticsearch_create('default', 'post', post.id)
46
+ post.save
47
+ end
48
+
49
+ it 'should send PUT request' do
50
+ last_request.method.should == 'PUT'
51
+ end
52
+
53
+ it 'should send request to _create verb for document resource' do
54
+ last_request.path.should == "/default/post/123/_create"
55
+ end
56
+
57
+ it 'should send document in body' do
58
+ last_request.body.should == post.elasticsearch_doc.to_json
59
+ end
60
+ end # context 'with unique ID'
61
+
62
+ context 'with duplicate ID' do
63
+ before do
64
+ stub_elasticsearch_create(
65
+ 'default', 'post', '123',
66
+ :body => {
67
+ 'error' => 'DocumentAlreadyExistsEngineException[[post][2] [post][1]]: document already exists',
68
+ 'status' => 409
69
+ }.to_json
70
+ )
71
+ end
72
+
73
+ let(:save) { lambda { post.save }}
74
+
75
+ it 'should raise DocumentAlreadyExistsEngineException' do
76
+ expect(&save).to raise_error(Elastictastic::ServerError::DocumentAlreadyExistsEngineException)
77
+ end
78
+
79
+ it 'should inject the error message into the exception' do
80
+ expect(&save).to raise_error { |error|
81
+ error.message.should == '[[post][2] [post][1]]: document already exists'
82
+ }
83
+ end
84
+
85
+ it 'should inject the status into the exception' do
86
+ expect(&save).to raise_error { |error|
87
+ error.status.should == 409
88
+ }
89
+ end
90
+ end # context 'with duplicate ID'
91
+ end # context 'new object with ID'
92
+
93
+ shared_examples_for 'persisted object' do
94
+ describe 'identity attributes' do
95
+ it 'should not allow setting of ID' do
96
+ lambda { post.id = 'bogus' }.should raise_error(Elastictastic::IllegalModificationError)
97
+ end
98
+ end # describe 'identity attributes'
99
+
100
+ describe '#save' do
101
+ before do
102
+ stub_elasticsearch_update('default', 'post', post.id)
103
+ post.title = 'Fun Factories for Fickle Ferrets'
104
+ post.save
105
+ end
106
+
107
+ it 'should send PUT request' do
108
+ last_request.method.should == 'PUT'
109
+ end
110
+
111
+ it "should send to document's resource path" do
112
+ last_request.path.should == "/default/post/#{post.id}"
113
+ end
114
+
115
+ it "should send document's body in request" do
116
+ last_request.body.should == post.elasticsearch_doc.to_json
117
+ end
118
+ end # describe '#save'
119
+ end # shared_examples_for 'persisted object'
120
+
121
+ context 'object after save' do
122
+ let(:post) do
123
+ stub_elasticsearch_create('default', 'post')
124
+ Post.new.tap { |post| post.save }
125
+ end
126
+
127
+ it_should_behave_like 'persisted object'
128
+ end # context 'object after save'
129
+
130
+ context 'existing persisted object' do
131
+ let(:post) do
132
+ Post.new.tap do |post|
133
+ post.id = '123'
134
+ post.persisted!
135
+ end
136
+ end
137
+
138
+ it_should_behave_like 'persisted object'
139
+ end # context 'existing persisted object'
140
+ end # describe '#save'
141
+
142
+ describe '#destroy' do
143
+ context 'existing persisted object' do
144
+ let(:post) do
145
+ Post.new.tap do |post|
146
+ post.id = '123'
147
+ post.persisted!
148
+ end
149
+ end
150
+
151
+ before do
152
+ stub_elasticsearch_destroy('default', 'post', '123')
153
+ @result = post.destroy
154
+ end
155
+
156
+ it 'should send DELETE request' do
157
+ last_request.method.should == 'DELETE'
158
+ end
159
+
160
+ it 'should send request to document resource path' do
161
+ last_request.path.should == '/default/post/123'
162
+ end
163
+
164
+ it 'should mark post as non-persisted' do
165
+ post.should_not be_persisted
166
+ end
167
+
168
+ it 'should return true' do
169
+ @result.should be_true
170
+ end
171
+ end # context 'existing persisted object'
172
+
173
+ context 'transient object' do
174
+ let(:post) { Post.new }
175
+
176
+ it 'should raise OperationNotAllowed' do
177
+ expect { post.destroy }.to raise_error(Elastictastic::OperationNotAllowed)
178
+ end
179
+ end # context 'transient object'
180
+
181
+ context 'non-existent persisted object' do
182
+ let(:post) do
183
+ Post.new.tap do |post|
184
+ post.id = '123'
185
+ post.persisted!
186
+ end
187
+ end
188
+
189
+ before do
190
+ stub_elasticsearch_destroy(
191
+ 'default', 'post', '123',
192
+ :body => {
193
+ 'ok' => true,
194
+ 'found' => false,
195
+ '_index' => 'default',
196
+ '_type' => 'post',
197
+ '_id' => '123',
198
+ '_version' => 0
199
+ }.to_json
200
+ )
201
+ @result = post.destroy
202
+ end
203
+
204
+ it 'should return false' do
205
+ @result.should be_false
206
+ end
207
+ end # describe 'non-existent persisted object'
208
+ end # describe '#destroy'
209
+
210
+ describe '::destroy_all' do
211
+ describe 'with default index' do
212
+ before do
213
+ stub_elasticsearch_destroy_all('default', 'post')
214
+ Post.destroy_all
215
+ end
216
+
217
+ it 'should send DELETE' do
218
+ last_request.method.should == 'DELETE'
219
+ end
220
+
221
+ it 'should send to index/type path' do
222
+ last_request.path.should == '/default/post'
223
+ end
224
+ end # describe 'with default index'
225
+
226
+ describe 'with specified index' do
227
+ before do
228
+ stub_elasticsearch_destroy_all('my_index', 'post')
229
+ Post.in_index('my_index').destroy_all
230
+ end
231
+
232
+ it 'should send to specified index' do
233
+ last_request.path.should == '/my_index/post'
234
+ end
235
+ end # describe 'with specified index'
236
+ end # describe '::destroy_all'
237
+
238
+ describe '::sync_mapping' do
239
+ shared_examples_for 'put mapping' do
240
+ it 'should send PUT request' do
241
+ last_request.method.should == 'PUT'
242
+ end
243
+
244
+ it 'should send mapping to ES' do
245
+ last_request.body.should == Post.mapping.to_json
246
+ end
247
+ end # shared_examples_for 'put mapping'
248
+
249
+ context 'with default index' do
250
+ before do
251
+ stub_elasticsearch_put_mapping('default', 'post')
252
+ Post.sync_mapping
253
+ end
254
+
255
+ it 'should send to resource path for mapping' do
256
+ last_request.path.should == '/default/post/_mapping'
257
+ end
258
+
259
+ it_should_behave_like 'put mapping'
260
+ end # context 'with default index'
261
+
262
+ context 'with specified index' do
263
+ before do
264
+ stub_elasticsearch_put_mapping('my_cool_index', 'post')
265
+ Post.in_index('my_cool_index').sync_mapping
266
+ end
267
+
268
+ it 'should send to specified index resource path' do
269
+ last_request.path.should == '/my_cool_index/post/_mapping'
270
+ end
271
+
272
+ it_should_behave_like 'put mapping'
273
+ end # context 'with specified index'
274
+ end # describe '::sync_mapping'
275
+
276
+ describe '::find' do
277
+
278
+ shared_examples_for 'single document lookup' do
279
+ context 'when document is found' do
280
+ before do
281
+ stub_elasticsearch_get(
282
+ index, 'post', '1',
283
+ )
284
+ end
285
+
286
+ it 'should return post instance' do
287
+ post.should be_a(Post)
288
+ end
289
+
290
+ it 'should request specified fields if specified' do
291
+ scope.fields('name', 'author.name').find(1)
292
+ last_request.path.should == "/#{index}/post/1?fields=name%2Cauthor.name"
293
+ end
294
+
295
+ it 'should return an array if id is passed in single-element array' do
296
+ posts = scope.find([1])
297
+ posts.should be_a(Array)
298
+ posts.first.id.should == '1'
299
+ end
300
+ end
301
+
302
+ context 'when document is not found' do
303
+ before do
304
+ stub_elasticsearch_get(index, 'post', '1', nil)
305
+ end
306
+
307
+ it 'should return nil' do
308
+ scope.find(1).should be_nil
309
+ end
310
+ end
311
+ end # shared_examples_for 'single document'
312
+
313
+ shared_examples_for 'multi document single index lookup' do
314
+
315
+ before do
316
+ stub_elasticsearch_mget(index, 'post', '1', '2')
317
+ posts
318
+ end
319
+
320
+ context 'with no options' do
321
+ let(:posts) { scope.find('1', '2') }
322
+
323
+ it 'should send request to index multiget endpoint' do
324
+ last_request.path.should == "/#{index}/post/_mget"
325
+ end
326
+
327
+ it 'should ask for IDs' do
328
+ last_request_body.should == {
329
+ 'docs' => [{ '_id' => '1'}, { '_id' => '2' }]
330
+ }
331
+ end
332
+
333
+ it 'should return documents' do
334
+ posts.map { |post| post.id }.should == %w(1 2)
335
+ end
336
+ end # context 'with no options'
337
+
338
+ context 'with fields option provided' do
339
+ let(:posts) { scope.fields('title').find('1', '2') }
340
+
341
+ it 'should send fields with each id' do
342
+ last_request_body.should == {
343
+ 'docs' => [
344
+ { '_id' => '1', 'fields' => %w(title) },
345
+ { '_id' => '2', 'fields' => %w(title) }
346
+ ]
347
+ }
348
+ end
349
+ end
350
+
351
+ context 'with multi-element array passed' do
352
+ let(:posts) { scope.find(%w(1 2)) }
353
+
354
+ it 'should request listed elements' do
355
+ last_request_body.should == {
356
+ 'docs' => [
357
+ { '_id' => '1' },
358
+ { '_id' => '2' }
359
+ ]
360
+ }
361
+ end
362
+ end
363
+ end # shared_examples_for 'multi document single index lookup'
364
+
365
+ context 'with default index' do
366
+ let(:scope) { Post }
367
+ let(:post) { Post.find(1) }
368
+ let(:index) { 'default' }
369
+
370
+ it_should_behave_like 'single document lookup'
371
+ it_should_behave_like 'multi document single index lookup'
372
+
373
+ describe 'multi-index multi-get' do
374
+ before do
375
+ stub_elasticsearch_mget(
376
+ nil,
377
+ nil,
378
+ ['1', 'default'], ['2', 'my_index'], ['3', 'my_index']
379
+ )
380
+ posts
381
+ end
382
+
383
+ describe 'with no options' do
384
+ let(:posts) { Post.find('default' => '1', 'my_index' => %w(2 3)) }
385
+
386
+ it 'should send request to base path' do
387
+ last_request.path.should == '/_mget'
388
+ end
389
+
390
+ it 'should request ids with type and index' do
391
+ last_request_body.should == {
392
+ 'docs' => [{
393
+ '_id' => '1',
394
+ '_type' => 'post',
395
+ '_index' => 'default'
396
+ }, {
397
+ '_id' => '2',
398
+ '_type' => 'post',
399
+ '_index' => 'my_index'
400
+ }, {
401
+ '_id' => '3',
402
+ '_type' => 'post',
403
+ '_index' => 'my_index'
404
+ }]
405
+ }
406
+ end
407
+
408
+ it 'should return docs with IDs' do
409
+ posts.map(&:id).should == %w(1 2 3)
410
+ end
411
+
412
+ it 'should set proper indices' do
413
+ posts.map { |post| post.index.name }.should ==
414
+ %w(default my_index my_index)
415
+ end
416
+ end # context 'with no options'
417
+
418
+ context 'with fields specified' do
419
+ let(:posts) { Post.fields('title').find('default' => '1', 'my_index' => %w(2 3)) }
420
+
421
+ it 'should inject fields into each identifier' do
422
+ last_request_body.should == {
423
+ 'docs' => [{
424
+ '_id' => '1',
425
+ '_type' => 'post',
426
+ '_index' => 'default',
427
+ 'fields' => %w(title)
428
+ }, {
429
+ '_id' => '2',
430
+ '_type' => 'post',
431
+ '_index' => 'my_index',
432
+ 'fields' => %w(title)
433
+ }, {
434
+ '_id' => '3',
435
+ '_type' => 'post',
436
+ '_index' => 'my_index',
437
+ 'fields' => %w(title)
438
+ }]
439
+ }
440
+ end
441
+ end
442
+ end # describe 'multi-index multi-get'
443
+
444
+ context 'when documents are missing' do
445
+ let(:posts) { Post.find('1', '2') }
446
+
447
+ before do
448
+ stub_elasticsearch_mget('default', 'post', '1' => {}, '2' => nil)
449
+ end
450
+
451
+ it 'should only return docs that exist' do
452
+ posts.map(&:id).should == ['1']
453
+ end
454
+ end
455
+ end # context 'with default index'
456
+
457
+ context 'with specified index' do
458
+ let(:scope) { Post.in_index('my_index') }
459
+ let(:post) { Post.in_index('my_index').find(1) }
460
+ let(:index) { 'my_index' }
461
+
462
+ it_should_behave_like 'single document lookup'
463
+ it_should_behave_like 'multi document single index lookup'
464
+ end # context 'with specified index'
465
+ end # describe '::find'
466
+
467
+ describe '::new_from_elasticsearch_hit' do
468
+ context 'with full _source' do
469
+ let :post do
470
+ Post.new.tap do |post|
471
+ post.elasticsearch_hit = {
472
+ '_id' => '1',
473
+ '_index' => 'my_index',
474
+ '_source' => {
475
+ 'title' => 'Testy time',
476
+ 'tags' => %w(search lucene),
477
+ 'author' => { 'name' => 'Mat Brown' },
478
+ 'comments' => [
479
+ { 'body' => 'first comment' },
480
+ { 'body' => 'lol' }
481
+ ],
482
+ 'created_at' => '2011-09-12T13:27:16.345Z',
483
+ 'published_at' => 1315848697123
484
+ }
485
+ }
486
+ end
487
+ end
488
+
489
+ it 'should populate id' do
490
+ post.id.should == '1'
491
+ end
492
+
493
+ it 'should populate index' do
494
+ post.index.name.should == 'my_index'
495
+ end
496
+
497
+ it 'should mark document perisistent' do
498
+ post.should be_persisted
499
+ end
500
+
501
+ it 'should populate scalar in document' do
502
+ post.title.should == 'Testy time'
503
+ end
504
+
505
+ it 'should populate time from formatted string' do
506
+ post.created_at.should == Time.gm(2011, 9, 12, 13, 27, BigDecimal.new("16.345"))
507
+ end
508
+
509
+ it 'should populate time from millis since epoch' do
510
+ post.published_at.should == Time.gm(2011, 9, 12, 17, 31, BigDecimal.new("37.123"))
511
+ end
512
+
513
+ it 'should populate array in document' do
514
+ post.tags.should == %w(search lucene)
515
+ end
516
+
517
+ it 'should populate embedded field' do
518
+ post.author.name.should == 'Mat Brown'
519
+ end
520
+
521
+ it 'should populate array of embedded objects' do
522
+ post.comments.map { |comment| comment.body }.should ==
523
+ ['first comment', 'lol']
524
+ end
525
+ end # context 'with full _source'
526
+
527
+ context 'with specified fields' do
528
+ let(:post) do
529
+ Post.new.tap do |post|
530
+ post.elasticsearch_hit = {
531
+ '_id' => '1',
532
+ '_index' => 'my_index',
533
+ '_type' => 'post',
534
+ 'fields' => {
535
+ 'title' => 'Get efficient',
536
+ '_source.comments_count' => 2,
537
+ '_source.author' => {
538
+ 'id' => '1',
539
+ 'name' => 'Pontificator',
540
+ 'email' => 'pontificator@blogosphere.biz'
541
+ },
542
+ '_source.comments' => [{
543
+ 'body' => '#1 fun'
544
+ }, {
545
+ 'body' => 'good fortune'
546
+ }]
547
+ }
548
+ }
549
+ end
550
+ end
551
+
552
+ it 'should populate scalar from stored field' do
553
+ post.title.should == 'Get efficient'
554
+ end
555
+
556
+ it 'should populate scalar from _source' do
557
+ post.comments_count.should == 2
558
+ end
559
+
560
+ it 'should populate single-valued embedded object' do
561
+ post.author.name.should == 'Pontificator'
562
+ end
563
+
564
+ it 'should populate multi-valued embedded objects' do
565
+ post.comments.map { |comment| comment.body }.should == [
566
+ '#1 fun',
567
+ 'good fortune'
568
+ ]
569
+ end
570
+ end
571
+
572
+ describe 'with missing values for requested fields' do
573
+ let(:post) do
574
+ Post.new.tap do |post|
575
+ post.elasticsearch_hit = {
576
+ '_id' => '1',
577
+ '_index' => 'my_index',
578
+ 'fields' => {
579
+ 'title' => nil,
580
+ 'author.name' => nil,
581
+ '_source.comments' => nil
582
+ }
583
+ }
584
+ end
585
+ end
586
+
587
+ it 'should set scalar from stored field to nil' do
588
+ post.title.should be_nil
589
+ end
590
+
591
+ it 'should set embedded field to nil' do
592
+ post.author.should be_nil
593
+ end
594
+
595
+ it 'should set object field from source ot nil' do
596
+ post.comments.should be_nil
597
+ end
598
+ end
599
+ end
600
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::MassAssignmentSecurity do
4
+ let(:post) { Post.new(:title => 'hey guy', :comments_count => 3) }
5
+
6
+ it 'should allow allowed attributes' do
7
+ post.title.should == 'hey guy'
8
+ end
9
+
10
+ it 'should not allow forbidden attributes' do
11
+ post.comments_count.should be_nil
12
+ end
13
+ end
@@ -0,0 +1,92 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'stringio'
3
+
4
+ describe Elastictastic::Middleware::LogRequests do
5
+ include Elastictastic::TestHelpers
6
+
7
+ let(:io) { StringIO.new }
8
+ let(:logger) { Logger.new(io) }
9
+ let(:config) do
10
+ Elastictastic::Configuration.new.tap do |config|
11
+ config.logger = logger
12
+ end
13
+ end
14
+ let(:client) { Elastictastic::Client.new(config) }
15
+
16
+ before do
17
+ now = Time.now
18
+ Time.stub(:now).and_return(now, now + 0.003)
19
+ end
20
+
21
+ it 'should log get requests to logger' do
22
+ FakeWeb.register_uri(:get, "http://localhost:9200/default/post/1", :body => '{}')
23
+ client.get('default', 'post', '1')
24
+ io.string.should == "ElasticSearch GET (3ms) /default/post/1\n"
25
+ end
26
+
27
+ it 'should log body of POST requests to logger' do
28
+ stub_elasticsearch_create('default', 'post')
29
+ client.create('default', 'post', nil, {})
30
+ io.string.should == "ElasticSearch POST (3ms) /default/post {}\n"
31
+ end
32
+ end
33
+
34
+ describe Elastictastic::Middleware::Rotor do
35
+ let(:config) do
36
+ Elastictastic::Configuration.new.tap do |config|
37
+ config.hosts = ['http://es1.local', 'http://es2.local']
38
+ end
39
+ end
40
+ let(:client) { Elastictastic::Client.new(config) }
41
+ let(:last_request) { FakeWeb.last_request }
42
+
43
+ it 'should alternate requests between hosts' do
44
+ expect do
45
+ 2.times do
46
+ 1.upto 2 do |i|
47
+ host_status(i => true)
48
+ client.get('default', 'post', '1')
49
+ end
50
+ end
51
+ end.not_to raise_error # We can't check the hostname of last_request in Fakeweb
52
+ end
53
+
54
+ context 'if one host fails' do
55
+ let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
56
+
57
+ before do
58
+ host_status(1 => false, 2 => true)
59
+ end
60
+
61
+ it 'should try the next host' do
62
+ client.get('default', 'post', '1').should == { 'success' => true }
63
+ end
64
+ end
65
+
66
+ context 'if all hosts fail' do
67
+ let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
68
+
69
+ before do
70
+ host_status(1 => false, 2 => false)
71
+ end
72
+
73
+ it 'should raise error if no hosts respond' do
74
+ expect { client.get('default', 'post', '1') }.to(raise_error Elastictastic::NoServerAvailable)
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def host_status(statuses)
81
+ FakeWeb.clean_registry
82
+ statuses.each_pair do |i, healthy|
83
+ url = "http://es#{i}.local/default/post/1"
84
+ if healthy
85
+ options = { :body => '{"success":true}' }
86
+ else
87
+ options = { :exception => Errno::ECONNREFUSED }
88
+ end
89
+ FakeWeb.register_uri(:get, url, options)
90
+ end
91
+ end
92
+ end