require 'config_helper' require 'securerandom' describe Elastic::AppSearch::Client do let(:engine_name) { "ruby-client-test-#{SecureRandom.hex}" } include_context 'App Search Credentials' let(:client) { Elastic::AppSearch::Client.new(client_options) } before(:all) do # Bootstraps a static engine for 'read-only' options that require indexing # across the test suite @static_engine_name = "ruby-client-test-static-#{SecureRandom.hex}" as_api_key = ConfigHelper.get_as_api_key as_host_identifier = ConfigHelper.get_as_host_identifier as_api_endpoint = ConfigHelper.get_as_api_endpoint client_options = ConfigHelper.get_client_options(as_api_key, as_host_identifier, as_api_endpoint) @static_client = Elastic::AppSearch::Client.new(client_options) @static_client.create_engine(@static_engine_name) @document1 = { 'id' => '1', 'title' => 'The Great Gatsby' } @document2 = { 'id' => '2', 'title' => 'Catcher in the Rye' } @documents = [@document1, @document2] @static_client.index_documents(@static_engine_name, @documents) # Wait until documents are indexed start = Time.now ready = false until (ready) sleep(3) results = @static_client.search(@static_engine_name, '') ready = true if results['results'].length == 2 ready = true if (Time.now - start).to_i >= 120 # Time out after 2 minutes end end after(:all) do @static_client.destroy_engine(@static_engine_name) end describe '#create_signed_search_key' do let(:key) { 'private-xxxxxxxxxxxxxxxxxxxx' } let(:api_key_name) { 'private-key' } let(:enforced_options) do { 'query' => 'cat' } end subject do Elastic::AppSearch::Client.create_signed_search_key(key, api_key_name, enforced_options) end it 'should build a valid jwt' do decoded_token = JWT.decode subject, key, true, { algorithm: 'HS256' } expect(decoded_token[0]['api_key_name']).to eq(api_key_name) expect(decoded_token[0]['query']).to eq('cat') end end describe 'Requests' do it 'should include client name and version in headers' do stub_request(:any, "#{client_options[:host_identifier]}.api.swiftype.com/api/as/v1/engines") client.list_engines expect(WebMock).to have_requested(:get, "https://#{client_options[:host_identifier]}.api.swiftype.com/api/as/v1/engines") .with( :headers => { 'X-Swiftype-Client' => 'elastic-app-search-ruby', 'X-Swiftype-Client-Version' => Elastic::AppSearch::VERSION } ) end end context 'Documents' do let(:document) { { 'url' => 'http://www.youtube.com/watch?v=v1uyQZNg2vE' } } before do client.create_engine(engine_name) rescue Elastic::AppSearch::BadRequest end after do client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord end describe '#index_document' do subject { client.index_document(engine_name, document) } it 'should return a processed document status hash' do expect(subject).to match('id' => anything) end context 'when the document has an id' do let(:id) { 'some_id' } let(:document) { { 'id' => id, 'url' => 'http://www.youtube.com/watch?v=v1uyQZNg2vE' } } it 'should return a processed document status hash with the same id' do expect(subject).to eq('id' => id) end end context 'when a document has processing errors' do let(:document) { { 'id' => 'too long' * 100 } } it 'should raise an error when the API returns errors in the response' do expect do subject end.to raise_error(Elastic::AppSearch::InvalidDocument, /Invalid field/) end end context 'when a document has a Ruby Time object' do let(:time_rfc3339) { '2018-01-01T01:01:01+00:00' } let(:time_object) { Time.parse(time_rfc3339) } let(:document) { { 'created_at' => time_object } } it 'should serialize the time object in RFC 3339' do response = subject expect(response).to have_key('id') document_id = response.fetch('id') expect do documents = client.get_documents(engine_name, [document_id]) expect(documents.size).to eq(1) expect(documents.first['created_at']).to eq(time_rfc3339) end.to_not raise_error end end end describe '#index_documents' do let(:documents) { [document, second_document] } let(:second_document_id) { 'another_id' } let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } } subject { client.index_documents(engine_name, documents) } it 'should return an array of document status hashes' do expect(subject).to match( [ { 'id' => anything, 'errors' => [] }, { 'id' => second_document_id, 'errors' => [] } ] ) end context 'when one of the documents has processing errors' do let(:second_document) { { 'id' => 'too long' * 100 } } it 'should return respective errors in an array of document processing hashes' do expect(subject).to match( [ { 'id' => anything, 'errors' => [] }, { 'id' => anything, 'errors' => ['Invalid field type: id must be less than 800 characters'] }, ] ) end end end describe '#update_documents' do let(:documents) { [document, second_document] } let(:second_document_id) { 'another_id' } let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } } let(:updates) do [ { 'id' => second_document_id, 'url' => 'https://www.example.com' } ] end subject { client.update_documents(engine_name, updates) } before do client.index_documents(engine_name, documents) end # Note that since indexing a document takes up to a minute, # we don't expect this to succeed, so we simply verify that # the request responded with the correct 'id', even though # the 'errors' object likely contains errors. it 'should update existing documents' do expect(subject).to match(['id' => second_document_id, 'errors' => anything]) end end describe '#get_documents' do let(:documents) { [first_document, second_document] } let(:first_document_id) { 'id' } let(:first_document) { { 'id' => first_document_id, 'url' => 'https://www.youtube.com/watch?v=v1uyQZNg2vE' } } let(:second_document_id) { 'another_id' } let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } } subject { client.get_documents(engine_name, [first_document_id, second_document_id]) } before do client.index_documents(engine_name, documents) end it 'will return documents by id' do response = subject expect(response.size).to eq(2) expect(response[0]['id']).to eq(first_document_id) expect(response[1]['id']).to eq(second_document_id) end end describe '#list_documents' do let(:documents) { [first_document, second_document] } let(:first_document_id) { 'id' } let(:first_document) { { 'id' => first_document_id, 'url' => 'https://www.youtube.com/watch?v=v1uyQZNg2vE' } } let(:second_document_id) { 'another_id' } let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } } before do client.index_documents(engine_name, documents) end context 'when no options are specified' do it 'will return all documents' do response = client.list_documents(engine_name) expect(response['results'].size).to eq(2) expect(response['results'][0]['id']).to eq(first_document_id) expect(response['results'][1]['id']).to eq(second_document_id) end end context 'when options are specified' do it 'will return all documents' do response = client.list_documents(engine_name, :page => { :size => 1, :current => 2 }) expect(response['results'].size).to eq(1) expect(response['results'][0]['id']).to eq(second_document_id) end end end end context 'Search' do describe '#search' do subject { @static_client.search(@static_engine_name, query, options) } let(:query) { '' } let(:options) { { 'page' => { 'size' => 2 } } } it 'should execute a search query' do expect(subject).to match( 'meta' => anything, 'results' => [anything, anything] ) end end describe '#multi_search' do subject { @static_client.multi_search(@static_engine_name, queries) } context 'when options are provided' do let(:queries) do [ { 'query' => 'gatsby', 'options' => { 'page' => { 'size' => 1 } } }, { 'query' => 'catcher', 'options' => { 'page' => { 'size' => 1 } } } ] end it 'should execute a multi search query' do response = subject expect(response).to match( [ { 'meta' => anything, 'results' => [{ 'id' => { 'raw' => '1' }, 'title' => anything, '_meta' => anything }] }, { 'meta' => anything, 'results' => [{ 'id' => { 'raw' => '2' }, 'title' => anything, '_meta' => anything }] } ] ) end end context 'when options are omitted' do let(:queries) do [ { 'query' => 'gatsby' }, { 'query' => 'catcher' } ] end it 'should execute a multi search query' do response = subject expect(response).to match( [ { 'meta' => anything, 'results' => [{ 'id' => { 'raw' => '1' }, 'title' => anything, '_meta' => anything }] }, { 'meta' => anything, 'results' => [{ 'id' => { 'raw' => '2' }, 'title' => anything, '_meta' => anything }] } ] ) end end context 'when a search is bad' do let(:queries) do [ { 'query' => 'cat', 'options' => { 'search_fields' => { 'taco' => {} } } }, { 'query' => 'dog', 'options' => { 'search_fields' => { 'body' => {} } } } ] end it 'should throw an appropriate error' do expect { subject }.to raise_error do |e| expect(e).to be_a(Elastic::AppSearch::BadRequest) expect(e.errors).to eq(['Search fields contains invalid field: taco', 'Search fields contains invalid field: body']) end end end end end context 'QuerySuggest' do describe '#query_suggestion' do let(:query) { 'cat' } let(:options) { { :size => 3, :types => { :documents => { :fields => ['title'] } } } } context 'when options are provided' do subject { @static_client.query_suggestion(@static_engine_name, query, options) } it 'should request query suggestions' do expect(subject).to match( 'meta' => anything, 'results' => anything ) end end context 'when options are omitted' do subject { @static_client.query_suggestion(@static_engine_name, query) } it 'should request query suggestions' do expect(subject).to match( 'meta' => anything, 'results' => anything ) end end end end context 'SearchSettings' do let(:default_settings) { { "search_fields" => { "id" => { "weight" => 1 } }, "result_fields" => {"id"=>{"raw"=>{}}}, "boosts" => {} } } let(:updated_settings) { { "search_fields" => { "id" => { "weight" => 3 } }, "result_fields" => {"id"=>{"raw"=>{}}}, "boosts" => {} } } before(:each) do client.create_engine(engine_name) rescue Elastic::AppSearch::BadRequest end after(:each) do client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord end describe '#show_settings' do subject { client.show_settings(engine_name) } it 'should return default settings' do expect(subject).to match(default_settings) end end describe '#update_settings' do subject { client.show_settings(engine_name) } before do client.update_settings(engine_name, updated_settings) end it 'should update search settings' do expect(subject).to match(updated_settings) end end describe '#reset_settings' do subject { client.show_settings(engine_name) } before do client.update_settings(engine_name, updated_settings) client.reset_settings(engine_name) end it 'should reset search settings' do expect(subject).to match(default_settings) end end end context 'Engines' do after do client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord end context '#create_engine' do it 'should create an engine when given a right set of parameters' do expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord) client.create_engine(engine_name) expect { client.get_engine(engine_name) }.to_not raise_error end it 'should accept an optional language parameter' do expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord) client.create_engine(engine_name, 'da') expect(client.get_engine(engine_name)).to match('name' => anything, 'type' => anything, 'language' => 'da') end it 'should return an engine object' do engine = client.create_engine(engine_name) expect(engine).to be_kind_of(Hash) expect(engine['name']).to eq(engine_name) end it 'should return an error when the engine name has already been taken' do client.create_engine(engine_name) expect { client.create_engine(engine_name) }.to raise_error do |e| expect(e).to be_a(Elastic::AppSearch::BadRequest) expect(e.errors).to eq(['Name is already taken']) end end end context '#list_engines' do it 'should return an array with a list of engines' do expect(client.list_engines['results']).to be_an(Array) end it 'should include the engine name in listed objects' do client.create_engine(engine_name) engines = client.list_engines['results'] expect(engines.find { |e| e['name'] == engine_name }).to_not be_nil end it 'should include the engine name in listed objects with pagination' do client.create_engine(engine_name) engines = client.list_engines(:current => 1, :size => 20)['results'] expect(engines.find { |e| e['name'] == engine_name }).to_not be_nil end end context '#destroy_engine' do it 'should destroy the engine if it exists' do client.create_engine(engine_name) expect { client.get_engine(engine_name) }.to_not raise_error client.destroy_engine(engine_name) expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord) end it 'should raise an error if the engine does not exist' do expect { client.destroy_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord) end end end context 'Configuration' do context 'host_identifier' do it 'sets the base url correctly' do client = Elastic::AppSearch::Client.new(:host_identifier => 'host-asdf', :api_key => 'foo') expect(client.api_endpoint).to eq('https://host-asdf.api.swiftype.com/api/as/v1/') end it 'sets the base url correctly using deprecated as_host_key' do client = Elastic::AppSearch::Client.new(:account_host_key => 'host-asdf', :api_key => 'foo') expect(client.api_endpoint).to eq('https://host-asdf.api.swiftype.com/api/as/v1/') end end end end