mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
501 lines
18 KiB
Ruby
501 lines
18 KiB
Ruby
# Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe ExternalDataSource do
|
|
describe '#execute' 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
|
|
|
|
context 'when search url is unsafe' do
|
|
let(:search_url) { 'http://linklocal.example.com' }
|
|
let(:resolved_ip) { '169.254.123.45' }
|
|
|
|
before do
|
|
allow(IPSocket).to receive(:getaddress).with('linklocal.example.com').and_return(resolved_ip)
|
|
end
|
|
|
|
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::SearchUrlUnsafe)
|
|
.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
|