external_data_source_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe ExternalDataSource do
  4. describe '#execute', db_adapter: :postgresql do
  5. context 'with ElasticSearch', searchindex: true do
  6. let(:data_option) do
  7. create(:object_manager_attribute_autocompletion_ajax_external_data_source, :elastic_search)
  8. .data_option
  9. end
  10. let(:searchterm) { SecureRandom.uuid }
  11. let(:user1) { create(:agent, firstname: searchterm) }
  12. let(:user2) { create(:agent, firstname: searchterm) }
  13. before do
  14. user1
  15. user2
  16. searchindex_model_reload([User])
  17. end
  18. it 'returns search results' do
  19. result = described_class.new(options: data_option, render_context: {}, term: searchterm).process
  20. expect(result).to eq([
  21. { value: user1.id.to_s, label: user1.email },
  22. { value: user2.id.to_s, label: user2.email }
  23. ])
  24. end
  25. end
  26. describe 'handling configuration errors' do
  27. let(:data_option) do
  28. create(:object_manager_attribute_autocompletion_ajax_external_data_source, search_url: search_url)
  29. .data_option
  30. end
  31. context 'when search url is nil' do
  32. let(:search_url) { nil }
  33. it 'raises error' do
  34. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  35. expect { instance.process }
  36. .to raise_error(
  37. an_instance_of(ExternalDataSource::Errors::SearchUrlMissingError)
  38. .and(having_attributes(external_data_source: instance))
  39. )
  40. end
  41. end
  42. context 'when search url is not parsable URI' do
  43. let(:search_url) { 'loremipsum' }
  44. it 'raises error' do
  45. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  46. expect { instance.process }
  47. .to raise_error(
  48. an_instance_of(ExternalDataSource::Errors::SearchUrlInvalidError)
  49. .and(having_attributes(external_data_source: instance))
  50. )
  51. end
  52. end
  53. context 'when search url is bad URI' do
  54. let(:search_url) { 'http://host.com?#{search.t' }
  55. it 'raises error' do
  56. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  57. expect { instance.process }
  58. .to raise_error(
  59. an_instance_of(ExternalDataSource::Errors::SearchUrlInvalidError)
  60. .and(having_attributes(external_data_source: instance))
  61. )
  62. end
  63. end
  64. end
  65. describe 'handling of external data source' do
  66. let(:instance) { described_class.new }
  67. let(:search_url) { 'https://dummyjson.com' }
  68. let(:data_option) do
  69. create(:object_manager_attribute_autocompletion_ajax_external_data_source, search_url: search_url, list_key: '')
  70. .data_option
  71. end
  72. before do
  73. allow(UserAgent).to receive(:get).and_return(UserAgent::Result.new(success: true, data: []))
  74. end
  75. context 'when search URL contains placeholders' do
  76. let(:ticket) { create(:ticket) }
  77. let(:search_url) { 'https://dummyjson.com/ticket/#{ticket.id}' } # rubocop:disable Lint/InterpolationCheck
  78. it 'replaces placeholders correctly' do
  79. described_class.new(options: data_option, render_context: { ticket: ticket }, term: 'term', limit: 1).process
  80. expect(UserAgent)
  81. .to have_received(:get)
  82. .with("https://dummyjson.com/ticket/#{ticket.id}", anything, anything)
  83. end
  84. end
  85. context 'when search term contains umlauts (#4980)' do
  86. let(:search_term) { 'bücher' }
  87. let(:search_url) { 'https://dummyjson.com/products/search?q=#{search.term}' } # rubocop:disable Lint/InterpolationCheck
  88. it 'properly URL encodes search term' do
  89. described_class.new(options: data_option, render_context: {}, term: search_term, limit: 1).process
  90. expect(UserAgent)
  91. .to have_received(:get)
  92. .with("https://dummyjson.com/products/search?q=#{ERB::Util.url_encode(search_term)}", anything, anything)
  93. end
  94. end
  95. context 'when http basic username and password present' do
  96. before do
  97. data_option[:http_basic_auth_username] = 'test_username'
  98. data_option[:http_basic_auth_password] = 'test_password'
  99. end
  100. it 'sets username and password' do
  101. described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
  102. expect(UserAgent)
  103. .to have_received(:get)
  104. .with(anything, anything, include(user: 'test_username', password: 'test_password'))
  105. end
  106. end
  107. context 'when bearer token present' do
  108. before do
  109. data_option[:bearer_token_auth] = 'test_bearer_token'
  110. end
  111. it 'sets authorization token' do
  112. described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
  113. expect(UserAgent)
  114. .to have_received(:get)
  115. .with(anything, anything, include(bearer_token: 'test_bearer_token'))
  116. end
  117. end
  118. context 'when SSL verification flag present' do
  119. before do
  120. data_option[:verify_ssl] = false
  121. end
  122. it 'sets SSL verification flag' do
  123. described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
  124. expect(UserAgent)
  125. .to have_received(:get)
  126. .with(anything, anything, include(verify_ssl: false))
  127. end
  128. end
  129. end
  130. context 'with mocked response' do
  131. let(:instance) { described_class.new }
  132. let(:data_option) do
  133. create(:object_manager_attribute_autocompletion_ajax_external_data_source,
  134. list_key: list_key,
  135. value_key: value_key,
  136. label_key: label_key)
  137. .data_option
  138. end
  139. before do
  140. allow_any_instance_of(described_class)
  141. .to receive(:fetch_json)
  142. .and_return(json_response)
  143. end
  144. context 'with simple structure' do
  145. let(:json_response) do
  146. {
  147. 'items' => [
  148. { 'id' => 1, 'name' => 'name 1' },
  149. { 'id' => 2, 'name' => 'name 2' },
  150. ]
  151. }
  152. end
  153. let(:list_key) { 'items' }
  154. let(:value_key) { 'id' }
  155. let(:label_key) { 'name' }
  156. it 'returns correct data' do
  157. result = described_class.new(options: data_option, render_context: {}, term: 'term').process
  158. expect(result).to eq([
  159. { value: 1, label: 'name 1' },
  160. { value: 2, label: 'name 2' },
  161. ])
  162. end
  163. it 'returns limited set' do
  164. result = described_class.new(options: data_option, render_context: {}, term: 'term', limit: 1).process
  165. expect(result).to eq([
  166. { value: 1, label: 'name 1' },
  167. ])
  168. end
  169. end
  170. context 'with minimal structure' do
  171. let(:json_response) do
  172. %w[foo bar]
  173. end
  174. let(:list_key) { '' }
  175. let(:value_key) { '' }
  176. let(:label_key) { '' }
  177. it 'returns correct data' do
  178. result = described_class.new(options: data_option, render_context: {}, term: 'term').process
  179. expect(result).to eq([
  180. { value: 'foo', label: 'foo' },
  181. { value: 'bar', label: 'bar' },
  182. ])
  183. end
  184. end
  185. context 'with complex structure' do
  186. let(:json_response) do
  187. {
  188. 'deadend' => 'yes',
  189. 'results' => {
  190. 'items' => [
  191. { 'data' => { 'id' => 1, 'name' => 'name 1' } },
  192. { 'data' => { 'id' => 2, 'name' => 'name 2' } },
  193. { 'data' => { 'id' => 3, 'name' => false } },
  194. { 'data' => { 'id' => 4, 'name' => true } },
  195. ]
  196. }
  197. }
  198. end
  199. let(:list_key) { 'results.items' }
  200. let(:value_key) { 'data.id' }
  201. let(:label_key) { 'data.name' }
  202. it 'returns correct data' do
  203. result = described_class.new(options: data_option, render_context: {}, term: 'term').process
  204. expect(result).to eq([
  205. { value: 1, label: 'name 1' },
  206. { value: 2, label: 'name 2' },
  207. { value: 3, label: false },
  208. { value: 4, label: true },
  209. ])
  210. end
  211. context 'when list points to string' do
  212. let(:list_key) { 'deadend' }
  213. it 'raises error' do
  214. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  215. expect { instance.process }
  216. .to raise_error(
  217. an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
  218. .and(having_attributes(
  219. external_data_source: having_attributes(
  220. json: json_response,
  221. parsed_items: be_nil
  222. ),
  223. message: 'Search result list key "deadend" is not an array.'
  224. ))
  225. )
  226. end
  227. end
  228. context 'when list points to hash' do
  229. let(:list_key) { 'results' }
  230. it 'raises error' do
  231. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  232. expect { instance.process }
  233. .to raise_error(
  234. an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
  235. .and(having_attributes(
  236. external_data_source: having_attributes(
  237. json: json_response,
  238. parsed_items: be_nil
  239. ),
  240. message: 'Search result list key "results" is not an array.'
  241. ))
  242. )
  243. end
  244. end
  245. context 'when list points to array member' do
  246. let(:list_key) { 'results.items.data' }
  247. it 'raises error' do
  248. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  249. expect { instance.process }
  250. .to raise_error(
  251. an_instance_of(ExternalDataSource::Errors::ListPathParsingError)
  252. .and(having_attributes(
  253. external_data_source: having_attributes(
  254. json: json_response,
  255. parsed_items: be_nil
  256. ),
  257. message: 'Search result list key "results.items.data" was not found.'
  258. ))
  259. )
  260. end
  261. end
  262. context 'when list points to non existant key' do
  263. let(:list_key) { 'nonexistant' }
  264. it 'raises error' do
  265. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  266. expect { instance.process }
  267. .to raise_error(
  268. an_instance_of(ExternalDataSource::Errors::ListPathParsingError)
  269. .and(having_attributes(
  270. external_data_source: having_attributes(
  271. json: json_response,
  272. parsed_items: be_nil
  273. ),
  274. message: 'Search result list key "nonexistant" was not found.'
  275. ))
  276. )
  277. end
  278. end
  279. context 'when list fails to pick root element' do
  280. let(:list_key) { '' }
  281. it 'raises error' do
  282. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  283. expect { instance.process }
  284. .to raise_error(
  285. an_instance_of(ExternalDataSource::Errors::ListNotArrayParsingError)
  286. .and(having_attributes(
  287. external_data_source: having_attributes(
  288. json: json_response,
  289. parsed_items: be_nil
  290. ),
  291. message: 'Search result list is not an array. Please provide search result list key.'
  292. ))
  293. )
  294. end
  295. end
  296. context 'when value points to hash' do
  297. let(:value_key) { 'data' }
  298. it 'raises error' do
  299. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  300. expect { instance.process }
  301. .to raise_error(
  302. an_instance_of(ExternalDataSource::Errors::ItemValueInvalidTypeParsingError)
  303. .and(having_attributes(
  304. external_data_source: having_attributes(
  305. json: json_response,
  306. parsed_items: json_response.dig('results', 'items')
  307. ),
  308. message: 'Search result value key "data" is not a string, number or boolean.'
  309. ))
  310. )
  311. end
  312. end
  313. context 'when value points to non existant key' do
  314. let(:value_key) { 'nonexistant' }
  315. it 'raises error' do
  316. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  317. expect { instance.process }
  318. .to raise_error(
  319. an_instance_of(ExternalDataSource::Errors::ItemValuePathParsingError)
  320. .and(having_attributes(
  321. external_data_source: having_attributes(
  322. json: json_response,
  323. parsed_items: json_response.dig('results', 'items')
  324. ),
  325. message: 'Search result value key "nonexistant" was not found.'
  326. ))
  327. )
  328. end
  329. end
  330. context 'when value fails to pick root element' do
  331. let(:value_key) { '' }
  332. it 'raises error' do
  333. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  334. expect { instance.process }
  335. .to raise_error(
  336. an_instance_of(ExternalDataSource::Errors::ItemValueInvalidTypeParsingError)
  337. .and(having_attributes(
  338. external_data_source: having_attributes(
  339. json: json_response,
  340. parsed_items: json_response.dig('results', 'items')
  341. ),
  342. message: 'Search result value is not a string, a number or a boolean. Please provide search result value key.'
  343. ))
  344. )
  345. end
  346. end
  347. context 'when label points to hash' do
  348. let(:label_key) { 'data' }
  349. it 'raises error' do
  350. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  351. expect { instance.process }
  352. .to raise_error(
  353. an_instance_of(ExternalDataSource::Errors::ItemLabelInvalidTypeParsingError)
  354. .and(having_attributes(
  355. external_data_source: having_attributes(
  356. json: json_response,
  357. parsed_items: json_response.dig('results', 'items')
  358. ),
  359. message: 'Search result label key "data" is not a string, number or boolean.'
  360. ))
  361. )
  362. end
  363. end
  364. context 'when label points to non existant key' do
  365. let(:label_key) { 'nonexistant' }
  366. it 'raises error' do
  367. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  368. expect { instance.process }
  369. .to raise_error(
  370. an_instance_of(ExternalDataSource::Errors::ItemLabelPathParsingError)
  371. .and(having_attributes(
  372. external_data_source: having_attributes(
  373. json: json_response,
  374. parsed_items: json_response.dig('results', 'items')
  375. ),
  376. message: 'Search result label key "nonexistant" was not found.'
  377. ))
  378. )
  379. end
  380. end
  381. context 'when label fails to pick root element' do
  382. let(:label_key) { '' }
  383. it 'raises error' do
  384. instance = described_class.new(options: data_option, render_context: {}, term: 'term')
  385. expect { instance.process }
  386. .to raise_error(
  387. an_instance_of(ExternalDataSource::Errors::ItemLabelInvalidTypeParsingError)
  388. .and(having_attributes(
  389. external_data_source: having_attributes(
  390. json: json_response,
  391. parsed_items: json_response.dig('results', 'items')
  392. ),
  393. message: 'Search result label is not a string, a number or a boolean. Please provide search result label key.'
  394. ))
  395. )
  396. end
  397. end
  398. end
  399. end
  400. end
  401. end