debian-mirror-gitlab/elasticsearch-rails/elasticsearch-model/spec/elasticsearch/model/indexing_spec.rb
2021-03-05 16:19:46 +05:30

919 lines
27 KiB
Ruby

require 'spec_helper'
describe Elasticsearch::Model::Indexing do
before(:all) do
class ::DummyIndexingModel
extend ActiveModel::Naming
extend Elasticsearch::Model::Naming::ClassMethods
extend Elasticsearch::Model::Indexing::ClassMethods
def self.foo
'bar'
end
end
class NotFound < Exception; end
end
after(:all) do
remove_classes(DummyIndexingModel, NotFound)
end
describe 'the Settings class' do
it 'should be convertible to a hash' do
expect(Elasticsearch::Model::Indexing::Settings.new(foo: 'bar').to_hash).to eq(foo: 'bar')
end
it 'should be convertible to json' do
expect(Elasticsearch::Model::Indexing::Settings.new(foo: 'bar').as_json).to eq(foo: 'bar')
end
end
describe '#settings' do
it 'returns an instance of the Settings class' do
expect(DummyIndexingModel.settings).to be_a(Elasticsearch::Model::Indexing::Settings)
end
context 'when the settings are updated' do
before do
DummyIndexingModel.settings(foo: 'boo')
DummyIndexingModel.settings(bar: 'bam')
end
it 'updates the settings on the class' do
expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam')
end
end
context 'when the settings are updated with a yml file' do
before do
DummyIndexingModel.settings File.open('spec/support/model.yml')
DummyIndexingModel.settings bar: 'bam'
end
it 'updates the settings on the class' do
expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam', 'baz' => 'qux')
end
end
context 'when the settings are updated with a json file' do
before do
DummyIndexingModel.settings File.open('spec/support/model.json')
DummyIndexingModel.settings bar: 'bam'
end
it 'updates the settings on the class' do
expect(DummyIndexingModel.settings.to_hash).to eq(foo: 'boo', bar: 'bam', 'baz' => 'qux', 'laz' => 'qux')
end
end
end
describe '#mappings' do
let(:expected_mapping_hash) do
{ :mytype => { foo: 'bar', :properties => {} } }
end
it 'returns an instance of the Mappings class' do
expect(DummyIndexingModel.mappings).to be_a(Elasticsearch::Model::Indexing::Mappings)
end
it 'raises an exception when there is no type passed to the #initialize method' do
expect {
Elasticsearch::Model::Indexing::Mappings.new
}.to raise_exception(ArgumentError)
end
it 'should be convertible to a hash' do
expect(Elasticsearch::Model::Indexing::Mappings.new(:mytype, { foo: 'bar' }).to_hash).to eq(expected_mapping_hash)
end
it 'should be convertible to json' do
expect(Elasticsearch::Model::Indexing::Mappings.new(:mytype, { foo: 'bar' }).as_json).to eq(expected_mapping_hash)
end
context 'when specific mappings are defined' do
let(:mappings) do
Elasticsearch::Model::Indexing::Mappings.new(:mytype)
end
before do
mappings.indexes :foo, { type: 'boolean', include_in_all: false }
mappings.indexes :bar
end
it 'creates the correct mapping definition' do
expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('boolean')
end
it 'uses text as the default type' do
expect(mappings.to_hash[:mytype][:properties][:bar][:type]).to eq('text')
end
context 'when mappings are defined for multiple fields' do
before do
mappings.indexes :my_field, type: 'text' do
indexes :raw, type: 'keyword'
end
end
it 'defines the mapping for all the fields' do
expect(mappings.to_hash[:mytype][:properties][:my_field][:type]).to eq('text')
expect(mappings.to_hash[:mytype][:properties][:my_field][:fields][:raw][:type]).to eq('keyword')
expect(mappings.to_hash[:mytype][:properties][:my_field][:fields][:raw][:properties]).to be_nil
end
end
context 'when embedded properties are defined' do
before do
mappings.indexes :foo do
indexes :bar
end
mappings.indexes :foo_object, type: 'object' do
indexes :bar
end
mappings.indexes :foo_nested, type: 'nested' do
indexes :bar
end
mappings.indexes :foo_nested_as_symbol, type: :nested do
indexes :bar
end
end
it 'defines mappings for the embedded properties' do
expect(mappings.to_hash[:mytype][:properties][:foo][:type]).to eq('object')
expect(mappings.to_hash[:mytype][:properties][:foo][:properties][:bar][:type]).to eq('text')
expect(mappings.to_hash[:mytype][:properties][:foo][:fields]).to be_nil
expect(mappings.to_hash[:mytype][:properties][:foo_object][:type]).to eq('object')
expect(mappings.to_hash[:mytype][:properties][:foo_object][:properties][:bar][:type]).to eq('text')
expect(mappings.to_hash[:mytype][:properties][:foo_object][:fields]).to be_nil
expect(mappings.to_hash[:mytype][:properties][:foo_nested][:type]).to eq('nested')
expect(mappings.to_hash[:mytype][:properties][:foo_nested][:properties][:bar][:type]).to eq('text')
expect(mappings.to_hash[:mytype][:properties][:foo_nested][:fields]).to be_nil
expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:type]).to eq(:nested)
expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:properties]).not_to be_nil
expect(mappings.to_hash[:mytype][:properties][:foo_nested_as_symbol][:fields]).to be_nil
end
end
end
context 'when the method is called on a class' do
before do
DummyIndexingModel.mappings(foo: 'boo')
DummyIndexingModel.mappings(bar: 'bam')
end
let(:expected_mappings_hash) do
{ _doc: { foo: "boo", bar: "bam", properties: {} } }
end
it 'sets the mappings' do
expect(DummyIndexingModel.mappings.to_hash).to eq(expected_mappings_hash)
end
context 'when the method is called with a block' do
before do
DummyIndexingModel.mapping do
indexes :foo, type: 'boolean'
end
end
it 'sets the mappings' do
expect(DummyIndexingModel.mapping.to_hash[:_doc][:properties][:foo][:type]).to eq('boolean')
end
end
end
end
describe 'instance methods' do
before(:all) do
class ::DummyIndexingModelWithCallbacks
extend Elasticsearch::Model::Indexing::ClassMethods
include Elasticsearch::Model::Indexing::InstanceMethods
def self.before_save(&block)
(@callbacks ||= {})[block.hash] = block
end
def changes_to_save
{:foo => ['One', 'Two']}
end
end
class ::DummyIndexingModelWithNoChanges
extend Elasticsearch::Model::Indexing::ClassMethods
include Elasticsearch::Model::Indexing::InstanceMethods
def self.before_save(&block)
(@callbacks ||= {})[block.hash] = block
end
def changes_to_save
{}
end
end
class ::DummyIndexingModelWithCallbacksAndCustomAsIndexedJson
extend Elasticsearch::Model::Indexing::ClassMethods
include Elasticsearch::Model::Indexing::InstanceMethods
def self.before_save(&block)
(@callbacks ||= {})[block.hash] = block
end
def changes_to_save
{:foo => ['A', 'B'], :bar => ['C', 'D']}
end
def as_indexed_json(options={})
{ :foo => 'B' }
end
end
class ::DummyIndexingModelWithOldDirty
extend Elasticsearch::Model::Indexing::ClassMethods
include Elasticsearch::Model::Indexing::InstanceMethods
def self.before_save(&block)
(@callbacks ||= {})[block.hash] = block
end
def changes
{:foo => ['One', 'Two']}
end
end
end
after(:all) do
Object.send(:remove_const, :DummyIndexingModelWithCallbacks) if defined?(DummyIndexingModelWithCallbacks)
Object.send(:remove_const, :DummyIndexingModelWithNoChanges) if defined?(DummyIndexingModelWithNoChanges)
Object.send(:remove_const, :DummyIndexingModelWithCallbacksAndCustomAsIndexedJson) if defined?(DummyIndexingModelWithCallbacksAndCustomAsIndexedJson)
Object.send(:remove_const, :DummyIndexingModelWithOldDirty) if defined?(DummyIndexingModelWithOldDirty)
end
context 'when the module is included' do
context 'when the model uses the old ActiveModel::Dirty' do
before do
DummyIndexingModelWithOldDirty.__send__ :include, Elasticsearch::Model::Indexing::InstanceMethods
end
it 'registers callbacks' do
expect(DummyIndexingModelWithOldDirty.instance_variable_get(:@callbacks)).not_to be_empty
end
let(:instance) do
DummyIndexingModelWithOldDirty.new
end
it 'sets the @__changed_model_attributes variable before the callback' do
DummyIndexingModelWithOldDirty.instance_variable_get(:@callbacks).each do |n, callback|
instance.instance_eval(&callback)
expect(instance.instance_variable_get(:@__changed_model_attributes)).to eq(foo: 'Two')
end
end
end
context 'when the model users the current ActiveModel::Dirty' do
before do
DummyIndexingModelWithCallbacks.__send__ :include, Elasticsearch::Model::Indexing::InstanceMethods
end
it 'registers callbacks' do
expect(DummyIndexingModelWithCallbacks.instance_variable_get(:@callbacks)).not_to be_empty
end
let(:instance) do
DummyIndexingModelWithCallbacks.new
end
it 'sets the @__changed_model_attributes variable before the callback' do
DummyIndexingModelWithCallbacks.instance_variable_get(:@callbacks).each do |n, callback|
instance.instance_eval(&callback)
expect(instance.instance_variable_get(:@__changed_model_attributes)).to eq(foo: 'Two')
end
end
end
end
describe '#index_document' do
before do
expect(instance).to receive(:client).and_return(client)
expect(instance).to receive(:as_indexed_json).and_return('JSON')
expect(instance).to receive(:index_name).and_return('foo')
expect(instance).to receive(:document_type).and_return('bar')
expect(instance).to receive(:id).and_return('1')
end
let(:client) do
double('client')
end
let(:instance) do
DummyIndexingModelWithCallbacks.new
end
context 'when no options are passed to the method' do
before do
expect(client).to receive(:index).with(index: 'foo', type: 'bar', id: '1', body: 'JSON').and_return(true)
end
it 'provides the method on an instance' do
expect(instance.index_document).to be(true)
end
end
context 'when extra options are passed to the method' do
before do
expect(client).to receive(:index).with(index: 'foo', type: 'bar', id: '1', body: 'JSON', parent: 'A').and_return(true)
end
it 'passes the extra options to the method call on the client' do
expect(instance.index_document(parent: 'A')).to be(true)
end
end
end
describe '#delete_document' do
before do
expect(instance).to receive(:client).and_return(client)
expect(instance).to receive(:index_name).and_return('foo')
expect(instance).to receive(:document_type).and_return('bar')
expect(instance).to receive(:id).and_return('1')
end
let(:client) do
double('client')
end
let(:instance) do
DummyIndexingModelWithCallbacks.new
end
context 'when no options are passed to the method' do
before do
expect(client).to receive(:delete).with(index: 'foo', type: 'bar', id: '1').and_return(true)
end
it 'provides the method on an instance' do
expect(instance.delete_document).to be(true)
end
end
context 'when extra options are passed to the method' do
before do
expect(client).to receive(:delete).with(index: 'foo', type: 'bar', id: '1', parent: 'A').and_return(true)
end
it 'passes the extra options to the method call on the client' do
expect(instance.delete_document(parent: 'A')).to be(true)
end
end
end
describe '#update_document' do
let(:client) do
double('client')
end
let(:instance) do
DummyIndexingModelWithCallbacks.new
end
context 'when no changes are present' do
before do
expect(instance).to receive(:index_document).and_return(true)
expect(client).to receive(:update).never
instance.instance_variable_set(:@__changed_model_attributes, nil)
end
it 'updates the document' do
expect(instance.update_document).to be(true)
end
end
context 'when changes are present' do
before do
allow(instance).to receive(:client).and_return(client)
allow(instance).to receive(:index_name).and_return('foo')
allow(instance).to receive(:document_type).and_return('bar')
allow(instance).to receive(:id).and_return('1')
end
context 'when the changes are included in the as_indexed_json representation' do
before do
instance.instance_variable_set(:@__changed_model_attributes, { foo: 'bar' })
expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { foo: 'bar' } }).and_return(true)
end
it 'updates the document' do
expect(instance.update_document).to be(true)
end
end
context 'when the changes are not all included in the as_indexed_json representation' do
let(:instance) do
DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new
end
before do
instance.instance_variable_set(:@__changed_model_attributes, {'foo' => 'B', 'bar' => 'D' })
expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { foo: 'B' } }).and_return(true)
end
it 'updates the document' do
expect(instance.update_document).to be(true)
end
end
context 'when none of the changes are included in the as_indexed_json representation' do
let(:instance) do
DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new
end
before do
instance.instance_variable_set(:@__changed_model_attributes, {'bar' => 'D' })
end
it 'does not update the document' do
expect(instance.update_document).to_not be(true)
end
end
context 'when there are partial updates' do
let(:instance) do
DummyIndexingModelWithCallbacksAndCustomAsIndexedJson.new
end
before do
instance.instance_variable_set(:@__changed_model_attributes, { 'foo' => { 'bar' => 'BAR'} })
expect(instance).to receive(:as_indexed_json).and_return('foo' => 'BAR')
expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { 'foo' => 'BAR' } }).and_return(true)
end
it 'updates the document' do
expect(instance.update_document).to be(true)
end
end
end
end
describe '#update_document_attributes' do
let(:client) do
double('client')
end
let(:instance) do
DummyIndexingModelWithCallbacks.new
end
context 'when changes are present' do
before do
expect(instance).to receive(:client).and_return(client)
expect(instance).to receive(:index_name).and_return('foo')
expect(instance).to receive(:document_type).and_return('bar')
expect(instance).to receive(:id).and_return('1')
instance.instance_variable_set(:@__changed_model_attributes, { author: 'john' })
end
context 'when no options are specified' do
before do
expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { title: 'green' } }).and_return(true)
end
it 'updates the document' do
expect(instance.update_document_attributes(title: 'green')).to be(true)
end
end
context 'when extra options are provided' do
before do
expect(client).to receive(:update).with(index: 'foo', type: 'bar', id: '1', body: { doc: { title: 'green' } }, refresh: true).and_return(true)
end
it 'updates the document' do
expect(instance.update_document_attributes({ title: 'green' }, refresh: true)).to be(true)
end
end
end
end
end
describe '#index_exists?' do
before do
expect(DummyIndexingModel).to receive(:client).and_return(client)
end
context 'when the index exists' do
let(:client) do
double('client', indices: double('indices', exists: true))
end
it 'returns true' do
expect(DummyIndexingModel.index_exists?).to be(true)
end
end
context 'when the index does not exists' do
let(:client) do
double('client', indices: double('indices', exists: false))
end
it 'returns false' do
expect(DummyIndexingModel.index_exists?).to be(false)
end
end
context 'when the index API raises an error' do
let(:client) do
double('client').tap do |cl|
expect(cl).to receive(:indices).and_raise(StandardError)
end
end
it 'returns false' do
expect(DummyIndexingModel.index_exists?).to be(false)
end
end
context 'when the indices.exists API raises an error' do
let(:client) do
double('client', indices: indices)
end
let(:indices) do
double('indices').tap do |ind|
expect(ind).to receive(:exists).and_raise(StandardError)
end
end
it 'returns false' do
expect(DummyIndexingModel.index_exists?).to be(false)
end
end
end
describe '#delete_index!' do
before(:all) do
class ::DummyIndexingModelForRecreate
extend ActiveModel::Naming
extend Elasticsearch::Model::Naming::ClassMethods
extend Elasticsearch::Model::Indexing::ClassMethods
end
end
after(:all) do
Object.send(:remove_const, :DummyIndexingModelForRecreate) if defined?(DummyIndexingModelForRecreate)
end
context 'when the index is not found' do
let(:client) do
double('client', indices: indices, transport: double('transport', { logger: nil }))
end
let(:indices) do
double('indices').tap do |ind|
expect(ind).to receive(:delete).and_raise(NotFound)
end
end
before do
expect(DummyIndexingModelForRecreate).to receive(:client).at_most(3).times.and_return(client)
end
context 'when the force option is true' do
it 'deletes the index without raising an exception' do
expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil
end
context 'when the client has a logger' do
let(:logger) do
Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
end
let(:client) do
double('client', indices: indices, transport: double('transport', { logger: logger }))
end
it 'deletes the index without raising an exception' do
expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil
end
it 'logs the message that the index is not found' do
expect(logger).to receive(:debug)
expect(DummyIndexingModelForRecreate.delete_index!(force: true)).to be_nil
end
end
end
context 'when the force option is not provided' do
it 'raises an exception' do
expect {
DummyIndexingModelForRecreate.delete_index!
}.to raise_exception(NotFound)
end
end
context 'when the exception is not NotFound' do
let(:indices) do
double('indices').tap do |ind|
expect(ind).to receive(:delete).and_raise(Exception)
end
end
it 'raises an exception' do
expect {
DummyIndexingModelForRecreate.delete_index!
}.to raise_exception(Exception)
end
end
end
context 'when an index name is provided in the options' do
before do
expect(DummyIndexingModelForRecreate).to receive(:client).and_return(client)
expect(indices).to receive(:delete).with(index: 'custom-foo')
end
let(:client) do
double('client', indices: indices)
end
let(:indices) do
double('indices', delete: true)
end
it 'uses the index name' do
expect(DummyIndexingModelForRecreate.delete_index!(index: 'custom-foo'))
end
end
end
describe '#create_index' do
before(:all) do
class ::DummyIndexingModelForCreate
extend ActiveModel::Naming
extend Elasticsearch::Model::Naming::ClassMethods
extend Elasticsearch::Model::Indexing::ClassMethods
index_name 'foo'
settings index: { number_of_shards: 1 } do
mappings do
indexes :foo, analyzer: 'keyword'
end
end
end
end
after(:all) do
Object.send(:remove_const, :DummyIndexingModelForCreate) if defined?(DummyIndexingModelForCreate)
end
let(:client) do
double('client', indices: indices)
end
let(:indices) do
double('indices')
end
context 'when the index does not exist' do
before do
expect(DummyIndexingModelForCreate).to receive(:client).and_return(client)
expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(false)
end
context 'when options are not provided' do
let(:expected_body) do
{ mappings: { _doc: { properties: { foo: { analyzer: 'keyword',
type: 'text' } } } },
settings: { index: { number_of_shards: 1 } } }
end
before do
expect(indices).to receive(:create).with(index: 'foo', body: expected_body).and_return(true)
end
it 'creates the index' do
expect(DummyIndexingModelForCreate.create_index!).to be(true)
end
end
context 'when options are provided' do
let(:expected_body) do
{ mappings: { foobar: { properties: { foo: { analyzer: 'bar' } } } },
settings: { index: { number_of_shards: 3 } } }
end
before do
expect(indices).to receive(:create).with(index: 'foobar', body: expected_body).and_return(true)
end
it 'creates the index' do
expect(DummyIndexingModelForCreate.create_index! \
index: 'foobar',
settings: { index: { number_of_shards: 3 } },
mappings: { foobar: { properties: { foo: { analyzer: 'bar' } } } }
).to be(true)
end
end
end
context 'when the index exists' do
before do
expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(true)
expect(indices).to receive(:create).never
end
it 'does not create the index' do
expect(DummyIndexingModelForCreate.create_index!).to be_nil
end
end
context 'when creating the index raises an exception' do
before do
expect(DummyIndexingModelForCreate).to receive(:client).and_return(client)
expect(DummyIndexingModelForCreate).to receive(:index_exists?).and_return(false)
expect(DummyIndexingModelForCreate).to receive(:delete_index!).and_return(true)
expect(indices).to receive(:create).and_raise(Exception)
end
it 'raises the exception' do
expect {
DummyIndexingModelForCreate.create_index!(force: true)
}.to raise_exception(Exception)
end
end
context 'when an index name is provided in the options' do
before do
expect(DummyIndexingModelForCreate).to receive(:client).and_return(client).twice
expect(indices).to receive(:exists).and_return(false)
expect(indices).to receive(:create).with(index: 'custom-foo', body: expected_body)
end
let(:expected_body) do
{ mappings: { _doc: { properties: { foo: { analyzer: 'keyword',
type: 'text' } } } },
settings: { index: { number_of_shards: 1 } } }
end
it 'uses the index name' do
expect(DummyIndexingModelForCreate.create_index!(index: 'custom-foo'))
end
end
context 'when the logging level is debug'
end
describe '#refresh_index!' do
before(:all) do
class ::DummyIndexingModelForRefresh
extend ActiveModel::Naming
extend Elasticsearch::Model::Naming::ClassMethods
extend Elasticsearch::Model::Indexing::ClassMethods
index_name 'foo'
settings index: { number_of_shards: 1 } do
mappings do
indexes :foo, analyzer: 'keyword'
end
end
end
end
after(:all) do
Object.send(:remove_const, :DummyIndexingModelForRefresh) if defined?(DummyIndexingModelForRefresh)
end
let(:client) do
double('client', indices: indices, transport: double('transport', { logger: nil }))
end
let(:indices) do
double('indices')
end
before do
expect(DummyIndexingModelForRefresh).to receive(:client).at_most(3).times.and_return(client)
end
context 'when the force option is true' do
context 'when the operation raises a NotFound exception' do
before do
expect(indices).to receive(:refresh).and_raise(NotFound)
end
it 'does not raise an exception' do
expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil
end
context 'when the client has a logger' do
let(:logger) do
Logger.new(STDOUT).tap { |l| l.level = Logger::DEBUG }
end
let(:client) do
double('client', indices: indices, transport: double('transport', { logger: logger }))
end
it 'does not raise an exception' do
expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil
end
it 'logs the message that the index is not found' do
expect(logger).to receive(:debug)
expect(DummyIndexingModelForRefresh.refresh_index!(force: true)).to be_nil
end
end
end
context 'when the operation raises another type of exception' do
before do
expect(indices).to receive(:refresh).and_raise(Exception)
end
it 'does not raise an exception' do
expect {
DummyIndexingModelForRefresh.refresh_index!(force: true)
}.to raise_exception(Exception)
end
end
end
context 'when an index name is provided in the options' do
before do
expect(indices).to receive(:refresh).with(index: 'custom-foo')
end
it 'uses the index name' do
expect(DummyIndexingModelForRefresh.refresh_index!(index: 'custom-foo'))
end
end
end
end