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.
- data/LICENSE +19 -0
- data/README.md +326 -0
- data/lib/elastictastic/association.rb +21 -0
- data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
- data/lib/elastictastic/callbacks.rb +30 -0
- data/lib/elastictastic/child_collection_proxy.rb +56 -0
- data/lib/elastictastic/client.rb +101 -0
- data/lib/elastictastic/configuration.rb +35 -0
- data/lib/elastictastic/dirty.rb +130 -0
- data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
- data/lib/elastictastic/document.rb +98 -0
- data/lib/elastictastic/errors.rb +7 -0
- data/lib/elastictastic/field.rb +38 -0
- data/lib/elastictastic/index.rb +19 -0
- data/lib/elastictastic/mass_assignment_security.rb +15 -0
- data/lib/elastictastic/middleware.rb +119 -0
- data/lib/elastictastic/nested_document.rb +29 -0
- data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
- data/lib/elastictastic/observer.rb +3 -0
- data/lib/elastictastic/observing.rb +21 -0
- data/lib/elastictastic/parent_child.rb +115 -0
- data/lib/elastictastic/persistence.rb +67 -0
- data/lib/elastictastic/properties.rb +236 -0
- data/lib/elastictastic/railtie.rb +35 -0
- data/lib/elastictastic/resource.rb +4 -0
- data/lib/elastictastic/scope.rb +283 -0
- data/lib/elastictastic/scope_builder.rb +32 -0
- data/lib/elastictastic/scoped.rb +20 -0
- data/lib/elastictastic/search.rb +180 -0
- data/lib/elastictastic/server_error.rb +15 -0
- data/lib/elastictastic/test_helpers.rb +172 -0
- data/lib/elastictastic/util.rb +63 -0
- data/lib/elastictastic/validations.rb +45 -0
- data/lib/elastictastic/version.rb +3 -0
- data/lib/elastictastic.rb +82 -0
- data/spec/environment.rb +6 -0
- data/spec/examples/active_model_lint_spec.rb +20 -0
- data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
- data/spec/examples/callbacks_spec.rb +96 -0
- data/spec/examples/dirty_spec.rb +238 -0
- data/spec/examples/document_spec.rb +600 -0
- data/spec/examples/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/middleware_spec.rb +92 -0
- data/spec/examples/observing_spec.rb +141 -0
- data/spec/examples/parent_child_spec.rb +308 -0
- data/spec/examples/properties_spec.rb +92 -0
- data/spec/examples/scope_spec.rb +491 -0
- data/spec/examples/search_spec.rb +382 -0
- data/spec/examples/spec_helper.rb +15 -0
- data/spec/examples/validation_spec.rb +65 -0
- data/spec/models/author.rb +9 -0
- data/spec/models/blog.rb +5 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/post.rb +41 -0
- data/spec/models/post_observer.rb +11 -0
- data/spec/support/fakeweb_request_history.rb +13 -0
- 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
|