123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe ExternalDataSource do
- describe '#execute', db_adapter: :postgresql do
- context 'with ElasticSearch', searchindex: true do
- let(:data_option) do
- create(:object_manager_attribute_autocompletion_ajax_external_data_source, :elastic_search)
- .data_option
- end
- let(:searchterm) { SecureRandom.uuid }
- let(:user1) { create(:agent, firstname: searchterm) }
- let(:user2) { create(:agent, firstname: searchterm) }
- before do
- user1
- user2
- searchindex_model_reload([User])
- end
- it 'returns search results' do
- result = described_class.new(options: data_option, render_context: {}, term: searchterm).process
- expect(result).to eq([
- { value: user1.id.to_s, label: user1.email },
- { value: user2.id.to_s, label: user2.email }
- ])
- end
- end
- describe 'handling configuration errors' do
- let(:data_option) do
- create(:object_manager_attribute_autocompletion_ajax_external_data_source, search_url: search_url)
- .data_option
- end
- context 'when search url is nil' do
- let(:search_url) { nil }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::SearchUrlMissingError)
- .and(having_attributes(external_data_source: instance))
- )
- end
- end
- context 'when search url is not parsable URI' do
- let(:search_url) { 'loremipsum' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::SearchUrlInvalidError)
- .and(having_attributes(external_data_source: instance))
- )
- end
- end
- context 'when search url is bad URI' do
- let(:search_url) { 'http://host.com?#{search.t' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::SearchUrlInvalidError)
- .and(having_attributes(external_data_source: instance))
- )
- end
- end
- end
- describe 'handling of external data source' do
- let(:instance) { described_class.new }
- let(:search_url) { 'https://dummyjson.com' }
- let(:data_option) do
- create(:object_manager_attribute_autocompletion_ajax_external_data_source, search_url: search_url, list_key: '')
- .data_option
- end
- before do
- allow(UserAgent).to receive(:get).and_return(UserAgent::Result.new(success: true, data: []))
- end
- context 'when search URL contains placeholders' do
- let(:ticket) { create(:ticket) }
- let(:search_url) { 'https://dummyjson.com/ticket/#{ticket.id}' } # rubocop:disable Lint/InterpolationCheck
- it 'replaces placeholders correctly' do
- described_class.new(options: data_option, render_context: { ticket: ticket }, term: 'term', limit: 1).process
- expect(UserAgent)
- .to have_received(:get)
- .with("https://dummyjson.com/ticket/#{ticket.id}", anything, anything)
- end
- end
- context 'when search term contains umlauts (#4980)' do
- let(:search_term) { 'bücher' }
- let(:search_url) { 'https://dummyjson.com/products/search?q=#{search.term}' } # rubocop:disable Lint/InterpolationCheck
- it 'properly URL encodes search term' do
- described_class.new(options: data_option, render_context: {}, term: search_term, limit: 1).process
- expect(UserAgent)
- .to have_received(:get)
- .with("https://dummyjson.com/products/search?q=#{ERB::Util.url_encode(search_term)}", anything, anything)
- end
- end
- context 'when http basic username and password present' do
- before do
- data_option[:http_basic_auth_username] = 'test_username'
- data_option[:http_basic_auth_password] = 'test_password'
- end
- it 'sets username and password' do
- described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
- expect(UserAgent)
- .to have_received(:get)
- .with(anything, anything, include(user: 'test_username', password: 'test_password'))
- end
- end
- context 'when bearer token present' do
- before do
- data_option[:bearer_token_auth] = 'test_bearer_token'
- end
- it 'sets authorization token' do
- described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
- expect(UserAgent)
- .to have_received(:get)
- .with(anything, anything, include(bearer_token: 'test_bearer_token'))
- end
- end
- context 'when SSL verification flag present' do
- before do
- data_option[:verify_ssl] = false
- end
- it 'sets SSL verification flag' do
- described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
- expect(UserAgent)
- .to have_received(:get)
- .with(anything, anything, include(verify_ssl: false))
- end
- end
- end
- context 'with mocked response' do
- let(:instance) { described_class.new }
- let(:data_option) do
- create(:object_manager_attribute_autocompletion_ajax_external_data_source,
- list_key: list_key,
- value_key: value_key,
- label_key: label_key)
- .data_option
- end
- before do
- allow_any_instance_of(described_class)
- .to receive(:fetch_json)
- .and_return(json_response)
- end
- context 'with simple structure' do
- let(:json_response) do
- {
- 'items' => [
- { 'id' => 1, 'name' => 'name 1' },
- { 'id' => 2, 'name' => 'name 2' },
- ]
- }
- end
- let(:list_key) { 'items' }
- let(:value_key) { 'id' }
- let(:label_key) { 'name' }
- it 'returns correct data' do
- result = described_class.new(options: data_option, render_context: {}, term: 'term').process
- expect(result).to eq([
- { value: 1, label: 'name 1' },
- { value: 2, label: 'name 2' },
- ])
- end
- it 'returns limited set' do
- result = described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
- expect(result).to eq([
- { value: 1, label: 'name 1' },
- ])
- end
- end
- context 'with minimal structure' do
- let(:json_response) do
- %w[foo bar]
- end
- let(:list_key) { '' }
- let(:value_key) { '' }
- let(:label_key) { '' }
- it 'returns correct data' do
- result = described_class.new(options: data_option, render_context: {}, term: 'term').process
- expect(result).to eq([
- { value: 'foo', label: 'foo' },
- { value: 'bar', label: 'bar' },
- ])
- end
- end
- context 'with complex structure' do
- let(:json_response) do
- {
- 'deadend' => 'yes',
- 'results' => {
- 'items' => [
- { 'data' => { 'id' => 1, 'name' => 'name 1' } },
- { 'data' => { 'id' => 2, 'name' => 'name 2' } },
- { 'data' => { 'id' => 3, 'name' => false } },
- { 'data' => { 'id' => 4, 'name' => true } },
- ]
- }
- }
- end
- let(:list_key) { 'results.items' }
- let(:value_key) { 'data.id' }
- let(:label_key) { 'data.name' }
- it 'returns correct data' do
- result = described_class.new(options: data_option, render_context: {}, term: 'term').process
- expect(result).to eq([
- { value: 1, label: 'name 1' },
- { value: 2, label: 'name 2' },
- { value: 3, label: false },
- { value: 4, label: true },
- ])
- end
- context 'when list points to string' do
- let(:list_key) { 'deadend' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: be_nil
- ),
- message: 'Search result list key "deadend" is not an array.'
- ))
- )
- end
- end
- context 'when list points to hash' do
- let(:list_key) { 'results' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: be_nil
- ),
- message: 'Search result list key "results" is not an array.'
- ))
- )
- end
- end
- context 'when list points to array member' do
- let(:list_key) { 'results.items.data' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ListPathParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: be_nil
- ),
- message: 'Search result list key "results.items.data" was not found.'
- ))
- )
- end
- end
- context 'when list points to non existant key' do
- let(:list_key) { 'nonexistant' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ListPathParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: be_nil
- ),
- message: 'Search result list key "nonexistant" was not found.'
- ))
- )
- end
- end
- context 'when list fails to pick root element' do
- let(:list_key) { '' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: be_nil
- ),
- message: 'Search result list is not an array. Please provide search result list key.'
- ))
- )
- end
- end
- context 'when value points to hash' do
- let(:value_key) { 'data' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemValueInvalidTypeParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result value key "data" is not a string, number or boolean.'
- ))
- )
- end
- end
- context 'when value points to non existant key' do
- let(:value_key) { 'nonexistant' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemValuePathParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result value key "nonexistant" was not found.'
- ))
- )
- end
- end
- context 'when value fails to pick root element' do
- let(:value_key) { '' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemValueInvalidTypeParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result value is not a string, a number or a boolean. Please provide search result value key.'
- ))
- )
- end
- end
- context 'when label points to hash' do
- let(:label_key) { 'data' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemLabelInvalidTypeParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result label key "data" is not a string, number or boolean.'
- ))
- )
- end
- end
- context 'when label points to non existant key' do
- let(:label_key) { 'nonexistant' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemLabelPathParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result label key "nonexistant" was not found.'
- ))
- )
- end
- end
- context 'when label fails to pick root element' do
- let(:label_key) { '' }
- it 'raises error' do
- instance = described_class.new(options: data_option, render_context: {}, term: 'term')
- expect { instance.process }
- .to raise_error(
- an_instance_of(ExternalDataSource::Errors::ItemLabelInvalidTypeParsingError)
- .and(having_attributes(
- external_data_source: having_attributes(
- json: json_response,
- parsed_items: json_response.dig('results', 'items')
- ),
- message: 'Search result label is not a string, a number or a boolean. Please provide search result label key.'
- ))
- )
- end
- end
- end
- end
- end
- end
|