search_index_backend_spec.rb 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe SearchIndexBackend do
  4. before do |example|
  5. next if !example.metadata[:searchindex]
  6. searchindex_model_reload([Ticket, User, Organization])
  7. end
  8. describe '.build_query' do
  9. subject(:query) { described_class.build_query('Ticket', '', query_extension: params) }
  10. let(:params) { { 'bool' => { 'filter' => { 'term' => { 'a' => 'b' } } } } }
  11. it 'coerces :query_extension hash keys to symbols' do
  12. expect(query.dig(:query, :bool, :filter, :term, :a)).to eq('b')
  13. end
  14. end
  15. describe '.search', searchindex: false do
  16. before do
  17. allow(described_class).to receive(:search_by_index) { |_query, index, _options| ["response:#{index}"] }
  18. end
  19. let(:query) { Faker::Lorem.word }
  20. let(:options) { { opt1: true } }
  21. it 'calls search_by_index if single index given' do
  22. described_class.search(query, 'Index A', options)
  23. expect(described_class)
  24. .to have_received(:search_by_index)
  25. .with(query, 'Index A', options)
  26. .once
  27. end
  28. it 'calls search_by_index for each given index given', aggregate_failures: true do
  29. described_class.search(query, %w[indexA indexB], options)
  30. expect(described_class)
  31. .to have_received(:search_by_index)
  32. .with(query, 'indexA', options)
  33. .once
  34. expect(described_class)
  35. .to have_received(:search_by_index)
  36. .with(query, 'indexB', options)
  37. .once
  38. end
  39. it 'flattens results if multiple indexes are queries' do
  40. expect(described_class.search(query, %w[indexA indexB], options))
  41. .to eq %w[response:indexA response:indexB]
  42. end
  43. context 'when one of the indexes return nil' do
  44. before do
  45. allow(described_class).to receive(:search_by_index)
  46. .with(anything, 'empty', anything).and_return(nil)
  47. end
  48. it 'does not include nil in flattened return' do
  49. expect(described_class.search(query, %w[indexA empty indexB], options))
  50. .to eq %w[response:indexA response:indexB]
  51. end
  52. it 'returns nil if single index was queried' do
  53. expect(described_class.search(query, 'empty', options))
  54. .to be_nil
  55. end
  56. end
  57. it 'raises an error if with_total_count option is passed' do
  58. expect { described_class.search(query, %w[indexA indexB], { with_total_count: true }) }
  59. .to raise_error(include('with_total_count'))
  60. end
  61. end
  62. describe '.search_by_index', searchindex: true do
  63. context 'query finds results' do
  64. let(:record_type) { 'Ticket'.freeze }
  65. let(:record) { create(:ticket) }
  66. before do
  67. record.search_index_update_backend
  68. described_class.refresh
  69. end
  70. it 'finds added records' do
  71. result = described_class.search_by_index(record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  72. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  73. end
  74. it 'returns count and id when with_total_count option is given' do
  75. result = described_class.search_by_index(record.number, record_type, with_total_count: true)
  76. expect(result).to include(
  77. total_count: 1,
  78. object_metadata: include(include(id: record.id.to_s))
  79. )
  80. end
  81. end
  82. context 'when search for user firstname + double lastname' do
  83. let(:record_type) { 'User'.freeze }
  84. let(:record) { create(:user, login: 'a', email: 'a@a.de', firstname: 'AnFirst', lastname: 'ASplit Lastname') }
  85. before do
  86. record.search_index_update_backend
  87. described_class.refresh
  88. end
  89. it 'finds user record' do
  90. result = described_class.search_by_index('AnFirst ASplit Lastname', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  91. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  92. end
  93. end
  94. context 'for query with no results' do
  95. subject(:search) { described_class.search_by_index(query, index, limit: 3000, with_total_count:) }
  96. let(:query) { 'preferences.notification_sound.enabled:*' }
  97. let(:with_total_count) { false }
  98. context 'on a single index' do
  99. let(:index) { 'User' }
  100. it { is_expected.to be_an(Array).and be_empty }
  101. context 'when with_total_count is given' do
  102. let(:with_total_count) { true }
  103. it { is_expected.to include(total_count: 0, object_metadata: be_empty) }
  104. end
  105. end
  106. context 'when user has a signature detection' do
  107. let(:user) { create(:agent, preferences: { signature_detection: 'Hamburg' }) }
  108. let(:record_type) { 'Ticket'.freeze }
  109. let(:record) { create(:ticket, created_by: user) }
  110. before do
  111. record.search_index_update_backend
  112. described_class.refresh
  113. end
  114. it 'does not find the ticket record' do
  115. result = described_class.search_by_index('Hamburg', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  116. expect(result).to eq([])
  117. end
  118. end
  119. end
  120. context 'search with date that requires time zone conversion', time_zone: 'Europe/Vilnius' do
  121. let(:record_type) { 'Ticket'.freeze }
  122. let(:record) { create(:ticket) }
  123. before do
  124. travel_to(Time.zone.parse('2019-01-02 00:33'))
  125. described_class.add(record_type, record)
  126. described_class.refresh
  127. end
  128. it 'finds record in a given timezone with a range' do
  129. Setting.set('timezone_default', 'UTC')
  130. result = described_class.search_by_index('created_at: [2019-01-01 TO 2019-01-01]', record_type)
  131. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  132. end
  133. it 'finds record in a far away timezone with a date' do
  134. Setting.set('timezone_default', 'Europe/Vilnius')
  135. result = described_class.search_by_index('created_at: 2019-01-02', record_type)
  136. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  137. end
  138. it 'finds record in UTC with date' do
  139. Setting.set('timezone_default', 'UTC')
  140. result = described_class.search_by_index('created_at: 2019-01-01', record_type)
  141. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  142. end
  143. end
  144. context 'does find integer values for ticket data', db_strategy: :reset do
  145. let(:record_type) { 'Ticket'.freeze }
  146. let(:record) { create(:ticket, inttest: '1021052349') }
  147. before do
  148. create(:object_manager_attribute_integer, name: 'inttest', data_option: {
  149. 'default' => 0,
  150. 'min' => 0,
  151. 'max' => 99_999_999,
  152. })
  153. ObjectManager::Attribute.migration_execute
  154. record.search_index_update_backend
  155. described_class.refresh
  156. end
  157. it 'finds added records by integer part' do
  158. result = described_class.search_by_index('102105', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  159. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  160. end
  161. it 'finds added records by integer' do
  162. result = described_class.search_by_index('1021052349', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  163. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  164. end
  165. it 'finds added records by quoted integer' do
  166. result = described_class.search_by_index('"1021052349"', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  167. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  168. end
  169. end
  170. context 'can sort by datetime fields', db_strategy: :reset do
  171. let(:record_type) { 'Ticket'.freeze }
  172. let(:record) { create(:ticket) }
  173. let(:field_name) { SecureRandom.uuid }
  174. before do
  175. create(:object_manager_attribute_datetime, name: field_name)
  176. ObjectManager::Attribute.migration_execute
  177. record.search_index_update_backend
  178. described_class.refresh
  179. end
  180. it 'finds added records' do
  181. result = described_class.search_by_index(record.number, record_type, sort_by: [field_name], order_by: ['desc'])
  182. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  183. end
  184. end
  185. end
  186. describe '.append_wildcard_to_simple_query' do
  187. context 'with "simple" queries' do
  188. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  189. M
  190. Max
  191. Max. # dot and underscore are acceptable characters in simple queries
  192. A_
  193. A_B
  194. äöü
  195. 123
  196. *ax # wildcards are allowed in simple queries
  197. Max*
  198. M*x
  199. M?x
  200. test@example.com
  201. test@example.
  202. test@example
  203. test@
  204. QUERIES
  205. it 'appends a * to the original query' do
  206. expect(queries.map { |query| described_class.append_wildcard_to_simple_query(query) })
  207. .to eq(queries.map { |q| "#{q}*" })
  208. end
  209. end
  210. context 'with "complex" queries (using search operators)' do
  211. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  212. title:"some words with spaces" # exact phrase / without quotation marks " an AND search for the words will be performed (in Zammad 1.5 and lower an OR search will be performed)
  213. title:"some wor*" # exact phrase beginning with "some wor*" will be searched
  214. created_at:[2017-01-01 TO 2017-12-31] # a time range
  215. created_at:>now-1h # created within last hour
  216. state:new OR state:open
  217. (state:new OR state:open) OR priority:"3 normal"
  218. (state:new OR state:open) AND customer.lastname:smith
  219. state:(new OR open) AND title:(full text search) # state: new OR open & title: full OR text OR search
  220. tags: "some tag"
  221. owner.email: "bod@example.com" AND state: (new OR open OR pending*) # show all open tickets of a certain agent
  222. state:closed AND _missing_:tag # all closed objects without tags
  223. article_count: [1 TO 5] # tickets with 1 to 5 articles
  224. article_count: [10 TO *] # tickets with 10 or more articles
  225. article.from: bob # also article.from can be used
  226. article.body: heat~ # using the fuzzy operator will also find terms that are similar, in this case also "head"
  227. article.body: /joh?n(ath[oa]n)/ # using regular expressions
  228. user:M
  229. user:Max
  230. user:Max.
  231. user:Max*
  232. organization:A_B
  233. organization:A_B*
  234. user: M
  235. user: Max
  236. user: Max.
  237. user: Max*
  238. organization: A_B
  239. organization: A_B*
  240. id:123
  241. number:123
  242. id:"123"
  243. number:"123"
  244. QUERIES
  245. it 'returns the original query verbatim' do
  246. expect(queries.map { |query| described_class.append_wildcard_to_simple_query(query) })
  247. .to eq(queries)
  248. end
  249. end
  250. end
  251. describe '.remove', searchindex: true do
  252. context 'record gets deleted' do
  253. let(:record_type) { 'Ticket'.freeze }
  254. let(:deleted_record) { create(:ticket) }
  255. before do
  256. described_class.add(record_type, deleted_record)
  257. described_class.refresh
  258. end
  259. it 'removes record from search index' do
  260. described_class.remove(record_type, deleted_record.id)
  261. described_class.refresh
  262. result = described_class.search(deleted_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  263. expect(result).to eq([])
  264. end
  265. context 'other records present' do
  266. let(:other_record) { create(:ticket) }
  267. before do
  268. described_class.add(record_type, other_record)
  269. described_class.refresh
  270. end
  271. it "doesn't remove other records" do
  272. described_class.remove(record_type, deleted_record.id)
  273. described_class.refresh
  274. result = described_class.search(other_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  275. expect(result).to eq([{ id: other_record.id.to_s, type: record_type }])
  276. end
  277. end
  278. end
  279. end
  280. describe '.selectors', searchindex: true do
  281. let(:initialize_object_manager_attributes) { nil }
  282. let(:group1) { create(:group) }
  283. let(:additional_ticket_attributes1) { {} }
  284. let(:organization1) { create(:organization, note: 'hihi') }
  285. let(:agent1) { create(:agent, organization: organization1, groups: [group1]) }
  286. let(:customer1) { create(:customer, organization: organization1, firstname: 'special-first-name') }
  287. let(:ticket1) do
  288. attributes = {
  289. title: 'some-title1',
  290. state_id: 1,
  291. created_by: agent1,
  292. group: group1,
  293. }
  294. ticket = create(:ticket, attributes.merge(additional_ticket_attributes1))
  295. ticket.tag_add('t1', 1)
  296. ticket
  297. end
  298. let(:ticket2) do
  299. ticket = create(:ticket, title: 'some_title2', state_id: 4)
  300. ticket.tag_add('t2', 1)
  301. ticket
  302. end
  303. let(:ticket3) do
  304. ticket = create(:ticket, title: 'some::title3', state_id: 1)
  305. ticket.tag_add('t1', 1)
  306. ticket.tag_add('t2', 1)
  307. ticket
  308. end
  309. let(:ticket4) { create(:ticket, title: 'phrase some-title4', state_id: 1) }
  310. let(:ticket5) { create(:ticket, title: 'phrase some_title5', state_id: 1) }
  311. let(:ticket6) { create(:ticket, title: 'phrase some::title6', state_id: 1) }
  312. let(:ticket7) { create(:ticket, title: 'some title7', state_id: 1) }
  313. let(:ticket8) { create(:ticket, title: 'sometitle', group: group1, state_id: 1, owner: agent1, customer: customer1, organization: organization1) }
  314. let(:article8) { create(:ticket_article, ticket: ticket8, subject: 'lorem ipsum') }
  315. before do
  316. Ticket.destroy_all # needed to remove not created tickets
  317. initialize_object_manager_attributes
  318. travel(-1.hour)
  319. create(:mention, mentionable: ticket1, user: agent1)
  320. ticket1.search_index_update_backend
  321. travel 1.hour
  322. ticket2.search_index_update_backend
  323. travel 1.second
  324. ticket3.search_index_update_backend
  325. travel 1.second
  326. ticket4.search_index_update_backend
  327. travel 1.second
  328. ticket5.search_index_update_backend
  329. travel 1.second
  330. ticket6.search_index_update_backend
  331. travel 1.second
  332. ticket7.search_index_update_backend
  333. travel 1.hour
  334. article8.ticket.search_index_update_backend
  335. described_class.refresh
  336. end
  337. context 'when limit is used' do
  338. it 'finds 1 record' do
  339. result = described_class.selectors('Ticket', { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }, { limit: 1 }, { field: 'created_at' })
  340. expect(result[:object_ids].count).to eq(1)
  341. end
  342. it 'finds 3 records' do
  343. result = described_class.selectors('Ticket', { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }, { limit: 3 }, { field: 'created_at' })
  344. expect(result[:object_ids].count).to eq(3)
  345. end
  346. end
  347. context 'query with contains' do
  348. it 'finds records with till (relative)' do
  349. result = described_class.selectors('Ticket',
  350. { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } },
  351. {},
  352. {
  353. field: 'created_at', # sort to verify result
  354. })
  355. expect(result).to eq({ count: 7, object_ids: [ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  356. end
  357. # https://github.com/zammad/zammad/issues/5105
  358. context 'with non-overlapping aggs_interval' do
  359. before do
  360. travel_to 18.months.from_now
  361. create(:ticket)
  362. searchindex_model_reload([Ticket])
  363. travel_back
  364. end
  365. it 'finds no records' do
  366. result = described_class.selectors('Ticket',
  367. { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } },
  368. {},
  369. {
  370. from: 1.year.from_now,
  371. to: 2.years.from_now,
  372. interval: 'month', # year, quarter, month, week, day, hour, minute, second
  373. field: 'created_at',
  374. })
  375. expect(result['hits']['total']['value']).to eq(0)
  376. end
  377. end
  378. it 'finds records with from (relative)' do
  379. result = described_class.selectors('Ticket',
  380. { 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } },
  381. {},
  382. {
  383. field: 'created_at', # sort to verify result
  384. })
  385. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  386. end
  387. it 'finds records with till (relative) including +1 hour ticket' do
  388. result = described_class.selectors('Ticket',
  389. { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '120', 'range' => 'minute' } },
  390. {},
  391. {
  392. field: 'created_at', # sort to verify result
  393. })
  394. expect(result).to eq({ count: 8, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  395. end
  396. it 'finds records with from (relative) including -1 hour ticket' do
  397. result = described_class.selectors('Ticket',
  398. { 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '120', 'range' => 'minute' } },
  399. {},
  400. {
  401. field: 'created_at', # sort to verify result
  402. })
  403. expect(result).to eq({ count: 8, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  404. end
  405. it 'finds records with tags which contains all' do
  406. result = described_class.selectors('Ticket',
  407. { 'ticket.tags'=>{ 'operator' => 'contains all', 'value' => 't1, t2' } },
  408. {},
  409. {
  410. field: 'created_at', # sort to verify result
  411. })
  412. expect(result).to eq({ count: 1, object_ids: [ticket3.id.to_s] })
  413. end
  414. it 'finds records with tags which contains one' do
  415. result = described_class.selectors('Ticket',
  416. { 'ticket.tags'=>{ 'operator' => 'contains one', 'value' => 't1, t2' } },
  417. {},
  418. {
  419. field: 'created_at', # sort to verify result
  420. })
  421. expect(result).to eq({ count: 3, object_ids: [ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  422. end
  423. it 'finds records with tags which contains all not' do
  424. result = described_class.selectors('Ticket',
  425. { 'ticket.tags'=>{ 'operator' => 'contains all not', 'value' => 't2' } },
  426. {},
  427. {
  428. field: 'created_at', # sort to verify result
  429. })
  430. expect(result).to eq({ count: 6, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket1.id.to_s] })
  431. end
  432. it 'finds records with tags which contains one not' do
  433. result = described_class.selectors('Ticket',
  434. { 'ticket.tags'=>{ 'operator' => 'contains one not', 'value' => 't1' } },
  435. {},
  436. {
  437. field: 'created_at', # sort to verify result
  438. })
  439. expect(result).to eq({ count: 6, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket2.id.to_s] })
  440. end
  441. it 'finds records with organization note' do
  442. result = described_class.selectors('Ticket',
  443. {
  444. 'organization.note' => {
  445. 'operator' => 'contains',
  446. 'value' => 'hihi',
  447. },
  448. },
  449. {},
  450. {
  451. field: 'created_at', # sort to verify result
  452. })
  453. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  454. end
  455. it 'finds records with customer firstname' do
  456. result = described_class.selectors('Ticket',
  457. {
  458. 'customer.firstname' => {
  459. 'operator' => 'contains',
  460. 'value' => 'special',
  461. },
  462. },
  463. {},
  464. {
  465. field: 'created_at', # sort to verify result
  466. })
  467. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  468. end
  469. it 'finds records with article subject' do
  470. result = described_class.selectors('Ticket',
  471. {
  472. 'article.subject' => {
  473. 'operator' => 'contains',
  474. 'value' => 'ipsum',
  475. },
  476. },
  477. {},
  478. {
  479. field: 'created_at', # sort to verify result
  480. })
  481. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  482. end
  483. it 'finds records with pre_condition not_set' do
  484. result = described_class.selectors('Ticket',
  485. {
  486. 'created_by_id' => {
  487. 'pre_condition' => 'not_set',
  488. 'operator' => 'is',
  489. },
  490. },
  491. {},
  492. {
  493. field: 'created_at', # sort to verify result
  494. })
  495. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  496. end
  497. it 'finds records with pre_condition current_user.id' do
  498. result = described_class.selectors('Ticket',
  499. {
  500. 'owner_id' => {
  501. 'pre_condition' => 'current_user.id',
  502. 'operator' => 'is',
  503. },
  504. },
  505. { current_user: agent1 },
  506. {
  507. field: 'created_at', # sort to verify result
  508. })
  509. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  510. end
  511. it 'finds records with pre_condition current_user.organization_id' do
  512. result = described_class.selectors('Ticket',
  513. {
  514. 'organization_id' => {
  515. 'pre_condition' => 'current_user.organization_id',
  516. 'operator' => 'is',
  517. },
  518. },
  519. { current_user: agent1 },
  520. {
  521. field: 'created_at', # sort to verify result
  522. })
  523. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  524. end
  525. it 'finds records with containing phrase' do
  526. result = described_class.selectors('Ticket',
  527. {
  528. 'title' => {
  529. 'operator' => 'contains',
  530. 'value' => 'phrase',
  531. },
  532. },
  533. {},
  534. {
  535. field: 'created_at', # sort to verify result
  536. })
  537. expect(result).to eq({ count: 3, object_ids: [ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s] })
  538. end
  539. it 'finds records with containing some title7' do
  540. result = described_class.selectors('Ticket',
  541. 'title' => {
  542. 'operator' => 'contains',
  543. 'value' => 'some title7',
  544. })
  545. expect(result).to eq({ count: 1, object_ids: [ticket7.id.to_s] })
  546. end
  547. it 'finds records with containing -' do
  548. result = described_class.selectors('Ticket',
  549. 'title' => {
  550. 'operator' => 'contains',
  551. 'value' => 'some-title1',
  552. })
  553. expect(result).to eq({ count: 1, object_ids: [ticket1.id.to_s] })
  554. end
  555. it 'finds records with containing _' do
  556. result = described_class.selectors('Ticket',
  557. 'title' => {
  558. 'operator' => 'contains',
  559. 'value' => 'some_title2',
  560. })
  561. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  562. end
  563. it 'finds records with containing ::' do
  564. result = described_class.selectors('Ticket',
  565. 'title' => {
  566. 'operator' => 'contains',
  567. 'value' => 'some::title3',
  568. })
  569. expect(result).to eq({ count: 1, object_ids: [ticket3.id.to_s] })
  570. end
  571. it 'finds records with containing 4' do
  572. result = described_class.selectors('Ticket',
  573. 'state_id' => {
  574. 'operator' => 'contains',
  575. 'value' => 4,
  576. })
  577. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  578. end
  579. it 'finds records with containing "4"' do
  580. result = described_class.selectors('Ticket',
  581. 'state_id' => {
  582. 'operator' => 'contains',
  583. 'value' => '4',
  584. })
  585. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  586. end
  587. end
  588. context 'query with contains not' do
  589. it 'finds records with containing not phrase' do
  590. result = described_class.selectors('Ticket',
  591. {
  592. 'title' => {
  593. 'operator' => 'contains not',
  594. 'value' => 'phrase',
  595. },
  596. },
  597. {},
  598. {
  599. field: 'created_at', # sort to verify result
  600. })
  601. expect(result).to eq({ count: 5, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  602. end
  603. it 'finds records with containing not some title7' do
  604. result = described_class.selectors('Ticket',
  605. 'title' => {
  606. 'operator' => 'contains not',
  607. 'value' => 'some title7',
  608. })
  609. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  610. end
  611. it 'finds records with containing not -' do
  612. result = described_class.selectors('Ticket',
  613. 'title' => {
  614. 'operator' => 'contains not',
  615. 'value' => 'some-title1',
  616. })
  617. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  618. end
  619. it 'finds records with containing not _' do
  620. result = described_class.selectors('Ticket',
  621. 'title' => {
  622. 'operator' => 'contains not',
  623. 'value' => 'some_title2',
  624. })
  625. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  626. end
  627. it 'finds records with containing not ::' do
  628. result = described_class.selectors('Ticket',
  629. 'title' => {
  630. 'operator' => 'contains not',
  631. 'value' => 'some::title3',
  632. })
  633. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  634. end
  635. it 'finds records with containing not 4' do
  636. result = described_class.selectors('Ticket',
  637. 'state_id' => {
  638. 'operator' => 'contains not',
  639. 'value' => 4,
  640. })
  641. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  642. end
  643. it 'finds records with containing not "4"' do
  644. result = described_class.selectors('Ticket',
  645. 'state_id' => {
  646. 'operator' => 'contains not',
  647. 'value' => '4',
  648. })
  649. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  650. end
  651. end
  652. context 'query with is' do
  653. it 'finds records with is phrase' do
  654. result = described_class.selectors('Ticket',
  655. 'title' => {
  656. 'operator' => 'is',
  657. 'value' => 'phrase',
  658. })
  659. expect(result).to eq({ count: 0, object_ids: [] })
  660. end
  661. it 'finds records with is some title7' do
  662. result = described_class.selectors('Ticket',
  663. 'title' => {
  664. 'operator' => 'is',
  665. 'value' => 'some title7',
  666. })
  667. expect(result).to eq({ count: 1, object_ids: [ticket7.id.to_s] })
  668. end
  669. it 'finds records with is -' do
  670. result = described_class.selectors('Ticket',
  671. 'title' => {
  672. 'operator' => 'is',
  673. 'value' => 'some-title1',
  674. })
  675. expect(result).to eq({ count: 1, object_ids: [ticket1.id.to_s] })
  676. end
  677. it 'finds records with is _' do
  678. result = described_class.selectors('Ticket',
  679. 'title' => {
  680. 'operator' => 'is',
  681. 'value' => 'some_title2',
  682. })
  683. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  684. end
  685. it 'finds records with is ::' do
  686. result = described_class.selectors('Ticket',
  687. 'title' => {
  688. 'operator' => 'is',
  689. 'value' => 'some::title3',
  690. })
  691. expect(result).to eq({ count: 1, object_ids: [ticket3.id.to_s] })
  692. end
  693. it 'finds records with is 4' do
  694. result = described_class.selectors('Ticket',
  695. 'state_id' => {
  696. 'operator' => 'is',
  697. 'value' => 4,
  698. })
  699. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  700. end
  701. it 'finds records with is "4"' do
  702. result = described_class.selectors('Ticket',
  703. 'state_id' => {
  704. 'operator' => 'is',
  705. 'value' => '4',
  706. })
  707. expect(result).to eq({ count: 1, object_ids: [ticket2.id.to_s] })
  708. end
  709. end
  710. context 'query with is not' do
  711. it 'finds records with is not phrase' do
  712. result = described_class.selectors('Ticket',
  713. 'title' => {
  714. 'operator' => 'is not',
  715. 'value' => 'phrase',
  716. })
  717. expect(result).to eq({ count: 8, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  718. end
  719. it 'finds records with is not some title7' do
  720. result = described_class.selectors('Ticket',
  721. 'title' => {
  722. 'operator' => 'is not',
  723. 'value' => 'some title7',
  724. })
  725. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  726. end
  727. it 'finds records with is not -' do
  728. result = described_class.selectors('Ticket',
  729. 'title' => {
  730. 'operator' => 'is not',
  731. 'value' => 'some-title1',
  732. })
  733. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  734. end
  735. it 'finds records with is not _' do
  736. result = described_class.selectors('Ticket',
  737. 'title' => {
  738. 'operator' => 'is not',
  739. 'value' => 'some_title2',
  740. })
  741. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  742. end
  743. it 'finds records with is not ::' do
  744. result = described_class.selectors('Ticket',
  745. 'title' => {
  746. 'operator' => 'is not',
  747. 'value' => 'some::title3',
  748. })
  749. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  750. end
  751. it 'finds records with is not 4' do
  752. result = described_class.selectors('Ticket',
  753. 'state_id' => {
  754. 'operator' => 'is not',
  755. 'value' => 4,
  756. })
  757. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  758. end
  759. it 'finds records with is not "4"' do
  760. result = described_class.selectors('Ticket',
  761. 'state_id' => {
  762. 'operator' => 'is not',
  763. 'value' => '4',
  764. })
  765. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket1.id.to_s] })
  766. end
  767. it 'finds records with is not state_id ["4"] and title ["sometitle"]' do
  768. result = described_class.selectors('Ticket',
  769. 'state_id' => {
  770. 'operator' => 'is not',
  771. 'value' => ['4'],
  772. },
  773. 'title' => {
  774. 'operator' => 'is',
  775. 'value' => ['sometitle'],
  776. })
  777. expect(result).to eq({ count: 1, object_ids: [ticket8.id.to_s] })
  778. end
  779. end
  780. context 'mentions' do
  781. it 'finds records with pre_condition is not_set' do
  782. result = described_class.selectors('Ticket',
  783. {
  784. 'ticket.mention_user_ids' => {
  785. 'pre_condition' => 'not_set',
  786. 'operator' => 'is',
  787. },
  788. },
  789. { current_user: agent1 },
  790. {
  791. field: 'created_at', # sort to verify result
  792. })
  793. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  794. end
  795. it 'finds records with pre_condition is not not_set' do
  796. result = described_class.selectors('Ticket',
  797. {
  798. 'ticket.mention_user_ids' => {
  799. 'pre_condition' => 'not_set',
  800. 'operator' => 'is not',
  801. },
  802. },
  803. { current_user: agent1 },
  804. {
  805. field: 'created_at', # sort to verify result
  806. })
  807. expect(result).to eq({ count: 1, object_ids: [ticket1.id.to_s] })
  808. end
  809. it 'finds records with pre_condition is current_user.id' do
  810. result = described_class.selectors('Ticket',
  811. {
  812. 'ticket.mention_user_ids' => {
  813. 'pre_condition' => 'current_user.id',
  814. 'operator' => 'is',
  815. },
  816. },
  817. { current_user: agent1 },
  818. {
  819. field: 'created_at', # sort to verify result
  820. })
  821. expect(result).to eq({ count: 1, object_ids: [ticket1.id.to_s] })
  822. end
  823. it 'finds records with pre_condition is not current_user.id' do
  824. result = described_class.selectors('Ticket',
  825. {
  826. 'ticket.mention_user_ids' => {
  827. 'pre_condition' => 'current_user.id',
  828. 'operator' => 'is not',
  829. },
  830. },
  831. { current_user: agent1 },
  832. {
  833. field: 'created_at', # sort to verify result
  834. })
  835. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  836. end
  837. it 'finds records with pre_condition is specific' do
  838. result = described_class.selectors('Ticket',
  839. {
  840. 'ticket.mention_user_ids' => {
  841. 'pre_condition' => 'specific',
  842. 'operator' => 'is',
  843. 'value' => agent1.id,
  844. },
  845. },
  846. {},
  847. {
  848. field: 'created_at', # sort to verify result
  849. })
  850. expect(result).to eq({ count: 1, object_ids: [ticket1.id.to_s] })
  851. end
  852. it 'finds records with pre_condition is not specific' do
  853. result = described_class.selectors('Ticket',
  854. {
  855. 'ticket.mention_user_ids' => {
  856. 'pre_condition' => 'specific',
  857. 'operator' => 'is not',
  858. 'value' => agent1.id,
  859. },
  860. },
  861. {},
  862. {
  863. field: 'created_at', # sort to verify result
  864. })
  865. expect(result).to eq({ count: 7, object_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s, ticket3.id.to_s, ticket2.id.to_s] })
  866. end
  867. end
  868. context 'with external data source field', db_adapter: :postgresql, db_strategy: :reset do
  869. let(:external_data_source_attribute) do
  870. create(:object_manager_attribute_autocompletion_ajax_external_data_source,
  871. name: 'external_data_source_attribute')
  872. end
  873. let(:name) { "ticket.#{external_data_source_attribute.name}" }
  874. let(:external_data_source_attribute_value) { 123 }
  875. let(:additional_ticket_attributes1) do
  876. {
  877. external_data_source_attribute.name => {
  878. value: external_data_source_attribute_value,
  879. label: 'Example'
  880. }
  881. }
  882. end
  883. let(:condition) do
  884. { operator: 'AND', conditions: [ {
  885. name: name,
  886. operator: operator,
  887. value: value,
  888. } ] }
  889. end
  890. let(:initialize_object_manager_attributes) do
  891. external_data_source_attribute
  892. ObjectManager::Attribute.migration_execute
  893. end
  894. describe "operator 'is'" do
  895. let(:operator) { 'is' }
  896. context 'with string' do
  897. context 'with matching string as value' do
  898. let(:external_data_source_attribute_value) { 'Example' }
  899. let(:value) do
  900. {
  901. value: 'Example',
  902. label: 'Example'
  903. }
  904. end
  905. it 'finds the ticket' do
  906. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  907. expect(result[:object_ids].count).to eq(1)
  908. end
  909. end
  910. context 'with non-matching string' do
  911. let(:value) do
  912. {
  913. value: 'Wrong',
  914. label: 'Wrong'
  915. }
  916. end
  917. it 'does not find the ticket' do
  918. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  919. expect(result[:object_ids].count).to eq(0)
  920. end
  921. end
  922. end
  923. context 'with matching integer as value' do
  924. let(:value) do
  925. {
  926. value: 123,
  927. label: 'Example'
  928. }
  929. end
  930. it 'finds the ticket' do
  931. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  932. expect(result[:object_ids].count).to eq(1)
  933. end
  934. end
  935. context 'with matching integer as value (but array style)' do
  936. let(:value) do
  937. [{
  938. value: 123,
  939. label: 'Example'
  940. }]
  941. end
  942. it 'finds the ticket' do
  943. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  944. expect(result[:object_ids].count).to eq(1)
  945. end
  946. end
  947. context 'with multiple values for matching' do
  948. let(:value) do
  949. [
  950. {
  951. value: 123,
  952. label: 'Example'
  953. },
  954. {
  955. value: 987,
  956. label: 'Example 2'
  957. }
  958. ]
  959. end
  960. it 'finds the ticket' do
  961. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  962. expect(result[:object_ids].count).to eq(1)
  963. end
  964. end
  965. context 'with matching boolean as value' do
  966. let(:external_data_source_attribute_value) { true }
  967. let(:value) do
  968. {
  969. value: true,
  970. label: 'Yes'
  971. }
  972. end
  973. it 'finds the ticket' do
  974. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  975. expect(result[:object_ids].count).to eq(1)
  976. end
  977. end
  978. end
  979. describe "operator 'is not'" do
  980. let(:operator) { 'is not' }
  981. context 'with matching integer as value' do
  982. let(:value) do
  983. {
  984. value: 986,
  985. label: 'Example'
  986. }
  987. end
  988. it 'find the ticket' do
  989. result = described_class.selectors('Ticket', condition, { current_user: agent1 })
  990. expect(result[:object_ids].count).to eq(8)
  991. end
  992. end
  993. end
  994. end
  995. end
  996. describe '.search_by_index_sort' do
  997. it 'does return a sort value' do
  998. expect(described_class.search_by_index_sort(index: 'Ticket', sort_by: ['title'], order_by: 'desc')).to eq([{ 'title.keyword'=>{ order: 'd' } }, '_score'])
  999. end
  1000. it 'does return a sort value for fulltext searches' do
  1001. expect(described_class.search_by_index_sort(index: 'Ticket', sort_by: ['title'], order_by: 'desc', fulltext: true)).to eq([{ 'title.keyword'=>{ order: 'd' } }, '_score'])
  1002. end
  1003. it 'does return a default sort value' do
  1004. expect(described_class.search_by_index_sort(index: 'Ticket')).to eq([{ updated_at: { order: 'desc' } }, '_score'])
  1005. end
  1006. it 'does return a default sort value for fulltext searches' do
  1007. expect(described_class.search_by_index_sort(index: 'Ticket', fulltext: true)).to eq(['_score'])
  1008. end
  1009. end
  1010. describe 'SSL verification', searchindex: true do
  1011. describe '.make_request' do
  1012. def request(verify: false)
  1013. Setting.set('es_url', 'https://127.0.0.1:9200')
  1014. Setting.set('es_ssl_verify', verify)
  1015. SearchIndexBackend.get('Ticket', Ticket.first.id)
  1016. end
  1017. it 'does verify SSL' do
  1018. allow(UserAgent).to receive(:get_http)
  1019. request(verify: true)
  1020. expect(UserAgent).to have_received(:get_http).with(URI::HTTPS, hash_including(verify_ssl: true)).once
  1021. end
  1022. it 'does not verify SSL' do
  1023. allow(UserAgent).to receive(:get_http)
  1024. request
  1025. expect(UserAgent).to have_received(:get_http).with(URI::HTTPS, hash_including(verify_ssl: false)).once
  1026. end
  1027. end
  1028. end
  1029. end