search_index_backend_spec.rb 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. # Copyright (C) 2012-2022 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. configure_elasticsearch(required: true, rebuild: true)
  7. end
  8. describe '.build_query' do
  9. subject(:query) { described_class.build_query('', 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: true do
  16. context 'query finds results' do
  17. let(:record_type) { 'Ticket'.freeze }
  18. let(:record) { create :ticket }
  19. before do
  20. record.search_index_update_backend
  21. described_class.refresh
  22. end
  23. it 'finds added records' do
  24. result = described_class.search(record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  25. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  26. end
  27. end
  28. context 'when search for user firstname + double lastname' do
  29. let(:record_type) { 'User'.freeze }
  30. let(:record) { create :user, login: 'a', email: 'a@a.de', firstname: 'AnFirst', lastname: 'ASplit Lastname' }
  31. before do
  32. record.search_index_update_backend
  33. described_class.refresh
  34. end
  35. it 'finds user record' do
  36. result = described_class.search('AnFirst ASplit Lastname', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  37. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  38. end
  39. end
  40. context 'for query with no results' do
  41. subject(:search) { described_class.search(query, index, limit: 3000) }
  42. let(:query) { 'preferences.notification_sound.enabled:*' }
  43. context 'on a single index' do
  44. let(:index) { 'User' }
  45. it { is_expected.to be_an(Array).and be_empty }
  46. end
  47. context 'on multiple indices' do
  48. let(:index) { %w[User Organization] }
  49. it { is_expected.to be_an(Array).and not_include(nil).and be_empty }
  50. end
  51. end
  52. context 'search with date that requires time zone conversion', time_zone: 'Europe/Vilnius' do
  53. let(:record_type) { 'Ticket'.freeze }
  54. let(:record) { create :ticket }
  55. before do
  56. travel_to(Time.zone.parse('2019-01-02 00:33'))
  57. described_class.add(record_type, record)
  58. described_class.refresh
  59. end
  60. it 'finds record in a given timezone with a range' do
  61. Setting.set('timezone_default', 'UTC')
  62. result = described_class.search('created_at: [2019-01-01 TO 2019-01-01]', record_type)
  63. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  64. end
  65. it 'finds record in a far away timezone with a date' do
  66. Setting.set('timezone_default', 'Europe/Vilnius')
  67. result = described_class.search('created_at: 2019-01-02', record_type)
  68. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  69. end
  70. it 'finds record in UTC with date' do
  71. Setting.set('timezone_default', 'UTC')
  72. result = described_class.search('created_at: 2019-01-01', record_type)
  73. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  74. end
  75. end
  76. context 'does find integer values for ticket data', db_strategy: :reset do
  77. let(:record_type) { 'Ticket'.freeze }
  78. let(:record) { create :ticket, inttest: '1021052349' }
  79. before do
  80. create(:object_manager_attribute_integer, name: 'inttest', data_option: {
  81. 'default' => 0,
  82. 'min' => 0,
  83. 'max' => 99_999_999,
  84. })
  85. ObjectManager::Attribute.migration_execute
  86. record.search_index_update_backend
  87. described_class.refresh
  88. end
  89. it 'finds added records by integer part' do
  90. result = described_class.search('102105', 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. it 'finds added records by integer' do
  94. result = described_class.search('1021052349', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  95. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  96. end
  97. it 'finds added records by quoted integer' do
  98. result = described_class.search('"1021052349"', record_type, sort_by: ['updated_at'], order_by: ['desc'])
  99. expect(result).to eq([{ id: record.id.to_s, type: record_type }])
  100. end
  101. end
  102. end
  103. describe '.append_wildcard_to_simple_query' do
  104. context 'with "simple" queries' do
  105. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  106. M
  107. Max
  108. Max. # dot and underscore are acceptable characters in simple queries
  109. A_
  110. A_B
  111. äöü
  112. 123
  113. *ax # wildcards are allowed in simple queries
  114. Max*
  115. M*x
  116. M?x
  117. test@example.com
  118. test@example.
  119. test@example
  120. test@
  121. QUERIES
  122. it 'appends a * to the original query' do
  123. expect(queries.map { |query| described_class.append_wildcard_to_simple_query(query) })
  124. .to eq(queries.map { |q| "#{q}*" })
  125. end
  126. end
  127. context 'with "complex" queries (using search operators)' do
  128. let(:queries) { <<~QUERIES.lines.map { |x| x.split('#')[0] }.map(&:strip) }
  129. 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)
  130. title:"some wor*" # exact phrase beginning with "some wor*" will be searched
  131. created_at:[2017-01-01 TO 2017-12-31] # a time range
  132. created_at:>now-1h # created within last hour
  133. state:new OR state:open
  134. (state:new OR state:open) OR priority:"3 normal"
  135. (state:new OR state:open) AND customer.lastname:smith
  136. state:(new OR open) AND title:(full text search) # state: new OR open & title: full OR text OR search
  137. tags: "some tag"
  138. owner.email: "bod@example.com" AND state: (new OR open OR pending*) # show all open tickets of a certain agent
  139. state:closed AND _missing_:tag # all closed objects without tags
  140. article_count: [1 TO 5] # tickets with 1 to 5 articles
  141. article_count: [10 TO *] # tickets with 10 or more articles
  142. article.from: bob # also article.from can be used
  143. article.body: heat~ # using the fuzzy operator will also find terms that are similar, in this case also "head"
  144. article.body: /joh?n(ath[oa]n)/ # using regular expressions
  145. user:M
  146. user:Max
  147. user:Max.
  148. user:Max*
  149. organization:A_B
  150. organization:A_B*
  151. user: M
  152. user: Max
  153. user: Max.
  154. user: Max*
  155. organization: A_B
  156. organization: A_B*
  157. id:123
  158. number:123
  159. id:"123"
  160. number:"123"
  161. QUERIES
  162. it 'returns the original query verbatim' do
  163. expect(queries.map { |query| described_class.append_wildcard_to_simple_query(query) })
  164. .to eq(queries)
  165. end
  166. end
  167. end
  168. describe '.remove', searchindex: true do
  169. context 'record gets deleted' do
  170. let(:record_type) { 'Ticket'.freeze }
  171. let(:deleted_record) { create :ticket }
  172. before do
  173. described_class.add(record_type, deleted_record)
  174. described_class.refresh
  175. end
  176. it 'removes record from search index' do
  177. described_class.remove(record_type, deleted_record.id)
  178. described_class.refresh
  179. result = described_class.search(deleted_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  180. expect(result).to eq([])
  181. end
  182. context 'other records present' do
  183. let(:other_record) { create :ticket }
  184. before do
  185. described_class.add(record_type, other_record)
  186. described_class.refresh
  187. end
  188. it "doesn't remove other records" do
  189. described_class.remove(record_type, deleted_record.id)
  190. described_class.refresh
  191. result = described_class.search(other_record.number, record_type, sort_by: ['updated_at'], order_by: ['desc'])
  192. expect(result).to eq([{ id: other_record.id.to_s, type: record_type }])
  193. end
  194. end
  195. end
  196. end
  197. describe '.selectors', searchindex: true do
  198. let(:group1) { create :group }
  199. let(:organization1) { create :organization, note: 'hihi' }
  200. let(:agent1) { create :agent, organization: organization1, groups: [group1] }
  201. let(:customer1) { create :customer, organization: organization1, firstname: 'special-first-name' }
  202. let(:ticket1) do
  203. ticket = create :ticket, title: 'some-title1', state_id: 1, created_by: agent1
  204. ticket.tag_add('t1', 1)
  205. ticket
  206. end
  207. let(:ticket2) do
  208. ticket = create :ticket, title: 'some_title2', state_id: 4
  209. ticket.tag_add('t2', 1)
  210. ticket
  211. end
  212. let(:ticket3) do
  213. ticket = create :ticket, title: 'some::title3', state_id: 1
  214. ticket.tag_add('t1', 1)
  215. ticket.tag_add('t2', 1)
  216. ticket
  217. end
  218. let(:ticket4) { create :ticket, title: 'phrase some-title4', state_id: 1 }
  219. let(:ticket5) { create :ticket, title: 'phrase some_title5', state_id: 1 }
  220. let(:ticket6) { create :ticket, title: 'phrase some::title6', state_id: 1 }
  221. let(:ticket7) { create :ticket, title: 'some title7', state_id: 1 }
  222. let(:ticket8) { create :ticket, title: 'sometitle', group: group1, state_id: 1, owner: agent1, customer: customer1, organization: organization1 }
  223. let(:article8) { create :ticket_article, ticket: ticket8, subject: 'lorem ipsum' }
  224. before do
  225. Ticket.destroy_all # needed to remove not created tickets
  226. travel(-1.hour)
  227. create(:mention, mentionable: ticket1, user: agent1)
  228. ticket1.search_index_update_backend
  229. travel 1.hour
  230. ticket2.search_index_update_backend
  231. travel 1.second
  232. ticket3.search_index_update_backend
  233. travel 1.second
  234. ticket4.search_index_update_backend
  235. travel 1.second
  236. ticket5.search_index_update_backend
  237. travel 1.second
  238. ticket6.search_index_update_backend
  239. travel 1.second
  240. ticket7.search_index_update_backend
  241. travel 1.hour
  242. article8.ticket.search_index_update_backend
  243. described_class.refresh
  244. end
  245. context 'query with contains' do
  246. it 'finds records with till (relative)' do
  247. result = described_class.selectors('Ticket',
  248. { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } },
  249. {},
  250. {
  251. field: 'created_at', # sort to verify result
  252. })
  253. expect(result).to eq({ count: 7, ticket_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] })
  254. end
  255. it 'finds records with from (relative)' do
  256. result = described_class.selectors('Ticket',
  257. { 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } },
  258. {},
  259. {
  260. field: 'created_at', # sort to verify result
  261. })
  262. expect(result).to eq({ count: 7, ticket_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] })
  263. end
  264. it 'finds records with till (relative) including +1 hour ticket' do
  265. result = described_class.selectors('Ticket',
  266. { 'ticket.created_at'=>{ 'operator' => 'till (relative)', 'value' => '120', 'range' => 'minute' } },
  267. {},
  268. {
  269. field: 'created_at', # sort to verify result
  270. })
  271. expect(result).to eq({ count: 8, ticket_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] })
  272. end
  273. it 'finds records with from (relative) including -1 hour ticket' do
  274. result = described_class.selectors('Ticket',
  275. { 'ticket.created_at'=>{ 'operator' => 'from (relative)', 'value' => '120', 'range' => 'minute' } },
  276. {},
  277. {
  278. field: 'created_at', # sort to verify result
  279. })
  280. expect(result).to eq({ count: 8, ticket_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] })
  281. end
  282. it 'finds records with tags which contains all' do
  283. result = described_class.selectors('Ticket',
  284. { 'ticket.tags'=>{ 'operator' => 'contains all', 'value' => 't1, t2' } },
  285. {},
  286. {
  287. field: 'created_at', # sort to verify result
  288. })
  289. expect(result).to eq({ count: 1, ticket_ids: [ticket3.id.to_s] })
  290. end
  291. it 'finds records with tags which contains one' do
  292. result = described_class.selectors('Ticket',
  293. { 'ticket.tags'=>{ 'operator' => 'contains one', 'value' => 't1, t2' } },
  294. {},
  295. {
  296. field: 'created_at', # sort to verify result
  297. })
  298. expect(result).to eq({ count: 3, ticket_ids: [ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  299. end
  300. it 'finds records with tags which contains all not' do
  301. result = described_class.selectors('Ticket',
  302. { 'ticket.tags'=>{ 'operator' => 'contains all not', 'value' => 't2' } },
  303. {},
  304. {
  305. field: 'created_at', # sort to verify result
  306. })
  307. expect(result).to eq({ count: 6, ticket_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] })
  308. end
  309. it 'finds records with tags which contains one not' do
  310. result = described_class.selectors('Ticket',
  311. { 'ticket.tags'=>{ 'operator' => 'contains one not', 'value' => 't1' } },
  312. {},
  313. {
  314. field: 'created_at', # sort to verify result
  315. })
  316. expect(result).to eq({ count: 6, ticket_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] })
  317. end
  318. it 'finds records with organization note' do
  319. result = described_class.selectors('Ticket',
  320. {
  321. 'organization.note' => {
  322. 'operator' => 'contains',
  323. 'value' => 'hihi',
  324. },
  325. },
  326. {},
  327. {
  328. field: 'created_at', # sort to verify result
  329. })
  330. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  331. end
  332. it 'finds records with customer firstname' do
  333. result = described_class.selectors('Ticket',
  334. {
  335. 'customer.firstname' => {
  336. 'operator' => 'contains',
  337. 'value' => 'special',
  338. },
  339. },
  340. {},
  341. {
  342. field: 'created_at', # sort to verify result
  343. })
  344. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  345. end
  346. it 'finds records with article subject' do
  347. result = described_class.selectors('Ticket',
  348. {
  349. 'article.subject' => {
  350. 'operator' => 'contains',
  351. 'value' => 'ipsum',
  352. },
  353. },
  354. {},
  355. {
  356. field: 'created_at', # sort to verify result
  357. })
  358. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  359. end
  360. it 'finds records with pre_condition not_set' do
  361. result = described_class.selectors('Ticket',
  362. {
  363. 'created_by_id' => {
  364. 'pre_condition' => 'not_set',
  365. 'operator' => 'is',
  366. },
  367. },
  368. {},
  369. {
  370. field: 'created_at', # sort to verify result
  371. })
  372. expect(result).to eq({ count: 7, ticket_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] })
  373. end
  374. it 'finds records with pre_condition current_user.id' do
  375. result = described_class.selectors('Ticket',
  376. {
  377. 'owner_id' => {
  378. 'pre_condition' => 'current_user.id',
  379. 'operator' => 'is',
  380. },
  381. },
  382. { current_user: agent1 },
  383. {
  384. field: 'created_at', # sort to verify result
  385. })
  386. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  387. end
  388. it 'finds records with pre_condition current_user.organization_id' do
  389. result = described_class.selectors('Ticket',
  390. {
  391. 'organization_id' => {
  392. 'pre_condition' => 'current_user.organization_id',
  393. 'operator' => 'is',
  394. },
  395. },
  396. { current_user: agent1 },
  397. {
  398. field: 'created_at', # sort to verify result
  399. })
  400. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  401. end
  402. it 'finds records with containing phrase' do
  403. result = described_class.selectors('Ticket',
  404. {
  405. 'title' => {
  406. 'operator' => 'contains',
  407. 'value' => 'phrase',
  408. },
  409. },
  410. {},
  411. {
  412. field: 'created_at', # sort to verify result
  413. })
  414. expect(result).to eq({ count: 3, ticket_ids: [ticket6.id.to_s, ticket5.id.to_s, ticket4.id.to_s] })
  415. end
  416. it 'finds records with containing some title7' do
  417. result = described_class.selectors('Ticket',
  418. 'title' => {
  419. 'operator' => 'contains',
  420. 'value' => 'some title7',
  421. })
  422. expect(result).to eq({ count: 1, ticket_ids: [ticket7.id.to_s] })
  423. end
  424. it 'finds records with containing -' do
  425. result = described_class.selectors('Ticket',
  426. 'title' => {
  427. 'operator' => 'contains',
  428. 'value' => 'some-title1',
  429. })
  430. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  431. end
  432. it 'finds records with containing _' do
  433. result = described_class.selectors('Ticket',
  434. 'title' => {
  435. 'operator' => 'contains',
  436. 'value' => 'some_title2',
  437. })
  438. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  439. end
  440. it 'finds records with containing ::' do
  441. result = described_class.selectors('Ticket',
  442. 'title' => {
  443. 'operator' => 'contains',
  444. 'value' => 'some::title3',
  445. })
  446. expect(result).to eq({ count: 1, ticket_ids: [ticket3.id.to_s] })
  447. end
  448. it 'finds records with containing 4' do
  449. result = described_class.selectors('Ticket',
  450. 'state_id' => {
  451. 'operator' => 'contains',
  452. 'value' => 4,
  453. })
  454. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  455. end
  456. it 'finds records with containing "4"' do
  457. result = described_class.selectors('Ticket',
  458. 'state_id' => {
  459. 'operator' => 'contains',
  460. 'value' => '4',
  461. })
  462. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  463. end
  464. end
  465. context 'query with contains not' do
  466. it 'finds records with containing not phrase' do
  467. result = described_class.selectors('Ticket',
  468. {
  469. 'title' => {
  470. 'operator' => 'contains not',
  471. 'value' => 'phrase',
  472. },
  473. },
  474. {},
  475. {
  476. field: 'created_at', # sort to verify result
  477. })
  478. expect(result).to eq({ count: 5, ticket_ids: [ticket8.id.to_s, ticket7.id.to_s, ticket3.id.to_s, ticket2.id.to_s, ticket1.id.to_s] })
  479. end
  480. it 'finds records with containing not some title7' do
  481. result = described_class.selectors('Ticket',
  482. 'title' => {
  483. 'operator' => 'contains not',
  484. 'value' => 'some title7',
  485. })
  486. expect(result).to eq({ count: 7, ticket_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] })
  487. end
  488. it 'finds records with containing not -' do
  489. result = described_class.selectors('Ticket',
  490. 'title' => {
  491. 'operator' => 'contains not',
  492. 'value' => 'some-title1',
  493. })
  494. expect(result).to eq({ count: 7, ticket_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] })
  495. end
  496. it 'finds records with containing not _' do
  497. result = described_class.selectors('Ticket',
  498. 'title' => {
  499. 'operator' => 'contains not',
  500. 'value' => 'some_title2',
  501. })
  502. expect(result).to eq({ count: 7, ticket_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] })
  503. end
  504. it 'finds records with containing not ::' do
  505. result = described_class.selectors('Ticket',
  506. 'title' => {
  507. 'operator' => 'contains not',
  508. 'value' => 'some::title3',
  509. })
  510. expect(result).to eq({ count: 7, ticket_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] })
  511. end
  512. it 'finds records with containing not 4' do
  513. result = described_class.selectors('Ticket',
  514. 'state_id' => {
  515. 'operator' => 'contains not',
  516. 'value' => 4,
  517. })
  518. expect(result).to eq({ count: 7, ticket_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] })
  519. end
  520. it 'finds records with containing not "4"' do
  521. result = described_class.selectors('Ticket',
  522. 'state_id' => {
  523. 'operator' => 'contains not',
  524. 'value' => '4',
  525. })
  526. expect(result).to eq({ count: 7, ticket_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] })
  527. end
  528. end
  529. context 'query with is' do
  530. it 'finds records with is phrase' do
  531. result = described_class.selectors('Ticket',
  532. 'title' => {
  533. 'operator' => 'is',
  534. 'value' => 'phrase',
  535. })
  536. expect(result).to eq({ count: 0, ticket_ids: [] })
  537. end
  538. it 'finds records with is some title7' do
  539. result = described_class.selectors('Ticket',
  540. 'title' => {
  541. 'operator' => 'is',
  542. 'value' => 'some title7',
  543. })
  544. expect(result).to eq({ count: 1, ticket_ids: [ticket7.id.to_s] })
  545. end
  546. it 'finds records with is -' do
  547. result = described_class.selectors('Ticket',
  548. 'title' => {
  549. 'operator' => 'is',
  550. 'value' => 'some-title1',
  551. })
  552. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  553. end
  554. it 'finds records with is _' do
  555. result = described_class.selectors('Ticket',
  556. 'title' => {
  557. 'operator' => 'is',
  558. 'value' => 'some_title2',
  559. })
  560. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  561. end
  562. it 'finds records with is ::' do
  563. result = described_class.selectors('Ticket',
  564. 'title' => {
  565. 'operator' => 'is',
  566. 'value' => 'some::title3',
  567. })
  568. expect(result).to eq({ count: 1, ticket_ids: [ticket3.id.to_s] })
  569. end
  570. it 'finds records with is 4' do
  571. result = described_class.selectors('Ticket',
  572. 'state_id' => {
  573. 'operator' => 'is',
  574. 'value' => 4,
  575. })
  576. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  577. end
  578. it 'finds records with is "4"' do
  579. result = described_class.selectors('Ticket',
  580. 'state_id' => {
  581. 'operator' => 'is',
  582. 'value' => '4',
  583. })
  584. expect(result).to eq({ count: 1, ticket_ids: [ticket2.id.to_s] })
  585. end
  586. end
  587. context 'query with is not' do
  588. it 'finds records with is not phrase' do
  589. result = described_class.selectors('Ticket',
  590. 'title' => {
  591. 'operator' => 'is not',
  592. 'value' => 'phrase',
  593. })
  594. expect(result).to eq({ count: 8, ticket_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] })
  595. end
  596. it 'finds records with is not some title7' do
  597. result = described_class.selectors('Ticket',
  598. 'title' => {
  599. 'operator' => 'is not',
  600. 'value' => 'some title7',
  601. })
  602. expect(result).to eq({ count: 7, ticket_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] })
  603. end
  604. it 'finds records with is not -' do
  605. result = described_class.selectors('Ticket',
  606. 'title' => {
  607. 'operator' => 'is not',
  608. 'value' => 'some-title1',
  609. })
  610. expect(result).to eq({ count: 7, ticket_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] })
  611. end
  612. it 'finds records with is not _' do
  613. result = described_class.selectors('Ticket',
  614. 'title' => {
  615. 'operator' => 'is not',
  616. 'value' => 'some_title2',
  617. })
  618. expect(result).to eq({ count: 7, ticket_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] })
  619. end
  620. it 'finds records with is not ::' do
  621. result = described_class.selectors('Ticket',
  622. 'title' => {
  623. 'operator' => 'is not',
  624. 'value' => 'some::title3',
  625. })
  626. expect(result).to eq({ count: 7, ticket_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] })
  627. end
  628. it 'finds records with is not 4' do
  629. result = described_class.selectors('Ticket',
  630. 'state_id' => {
  631. 'operator' => 'is not',
  632. 'value' => 4,
  633. })
  634. expect(result).to eq({ count: 7, ticket_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] })
  635. end
  636. it 'finds records with is not "4"' do
  637. result = described_class.selectors('Ticket',
  638. 'state_id' => {
  639. 'operator' => 'is not',
  640. 'value' => '4',
  641. })
  642. expect(result).to eq({ count: 7, ticket_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] })
  643. end
  644. it 'finds records with is not state_id ["4"] and title ["sometitle"]' do
  645. result = described_class.selectors('Ticket',
  646. 'state_id' => {
  647. 'operator' => 'is not',
  648. 'value' => ['4'],
  649. },
  650. 'title' => {
  651. 'operator' => 'is',
  652. 'value' => ['sometitle'],
  653. })
  654. expect(result).to eq({ count: 1, ticket_ids: [ticket8.id.to_s] })
  655. end
  656. end
  657. context 'mentions' do
  658. it 'finds records with pre_condition is not_set' do
  659. result = described_class.selectors('Ticket',
  660. {
  661. 'ticket.mention_user_ids' => {
  662. 'pre_condition' => 'not_set',
  663. 'operator' => 'is',
  664. },
  665. },
  666. { current_user: agent1 },
  667. {
  668. field: 'created_at', # sort to verify result
  669. })
  670. expect(result).to eq({ count: 7, ticket_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] })
  671. end
  672. it 'finds records with pre_condition is not not_set' do
  673. result = described_class.selectors('Ticket',
  674. {
  675. 'ticket.mention_user_ids' => {
  676. 'pre_condition' => 'not_set',
  677. 'operator' => 'is not',
  678. },
  679. },
  680. { current_user: agent1 },
  681. {
  682. field: 'created_at', # sort to verify result
  683. })
  684. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  685. end
  686. it 'finds records with pre_condition is current_user.id' do
  687. result = described_class.selectors('Ticket',
  688. {
  689. 'ticket.mention_user_ids' => {
  690. 'pre_condition' => 'current_user.id',
  691. 'operator' => 'is',
  692. },
  693. },
  694. { current_user: agent1 },
  695. {
  696. field: 'created_at', # sort to verify result
  697. })
  698. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  699. end
  700. it 'finds records with pre_condition is not current_user.id' do
  701. result = described_class.selectors('Ticket',
  702. {
  703. 'ticket.mention_user_ids' => {
  704. 'pre_condition' => 'current_user.id',
  705. 'operator' => 'is not',
  706. },
  707. },
  708. { current_user: agent1 },
  709. {
  710. field: 'created_at', # sort to verify result
  711. })
  712. expect(result).to eq({ count: 7, ticket_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] })
  713. end
  714. it 'finds records with pre_condition is specific' do
  715. result = described_class.selectors('Ticket',
  716. {
  717. 'ticket.mention_user_ids' => {
  718. 'pre_condition' => 'specific',
  719. 'operator' => 'is',
  720. 'value' => agent1.id,
  721. },
  722. },
  723. {},
  724. {
  725. field: 'created_at', # sort to verify result
  726. })
  727. expect(result).to eq({ count: 1, ticket_ids: [ticket1.id.to_s] })
  728. end
  729. it 'finds records with pre_condition is not specific' do
  730. result = described_class.selectors('Ticket',
  731. {
  732. 'ticket.mention_user_ids' => {
  733. 'pre_condition' => 'specific',
  734. 'operator' => 'is not',
  735. 'value' => agent1.id,
  736. },
  737. },
  738. {},
  739. {
  740. field: 'created_at', # sort to verify result
  741. })
  742. expect(result).to eq({ count: 7, ticket_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] })
  743. end
  744. end
  745. end
  746. describe '.verify_date_range' do
  747. let(:range_1) { { range: { created_at: { from: '2020-01-01T00:00:00.000Z', to: '2021-12-31T23:59:59Z' } } } }
  748. let(:range_2) { { range: { created_at: { from: '2020-03-01T00:00:00.000Z', to: '2020-03-31T23:59:59Z' } } } }
  749. let(:range_3) { { range: { created_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
  750. let(:range_4) { { range: { updated_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } }
  751. def build_payload(*ranges)
  752. {
  753. query: {
  754. bool: {
  755. must: ranges
  756. }
  757. }
  758. }
  759. end
  760. it 'verifies single range' do
  761. result = described_class.verify_date_range 'url', build_payload(range_1)
  762. expect(result).to be_truthy
  763. end
  764. it 'verifies multiple intersecting ranges' do
  765. result = described_class.verify_date_range 'url', build_payload(range_1, range_2)
  766. expect(result).to be_truthy
  767. end
  768. it 'verifies non-intersecting ranges on different keys' do
  769. result = described_class.verify_date_range 'url', build_payload(range_1, range_4)
  770. expect(result).to be_truthy
  771. end
  772. it 'verifies payload without any ranges' do
  773. result = described_class.verify_date_range 'url', build_payload
  774. expect(result).to be_truthy
  775. end
  776. it 'verifies payload without payload' do
  777. result = described_class.verify_date_range 'url', {}
  778. expect(result).to be_truthy
  779. end
  780. it 'raises an error with multiple non-intersecting range' do
  781. expect { described_class.verify_date_range 'url', build_payload(range_1, range_3) }
  782. .to raise_error(%r{Conflicting date ranges})
  783. end
  784. context 'with a stubbed range' do
  785. before do
  786. allow(described_class).to receive(:convert_es_date_range).and_return(mock_range)
  787. end
  788. let(:mock_range) { instance_double('Range', overlaps?: true) }
  789. it 'checks overlap once for 2 ranges' do
  790. described_class.verify_date_range 'url', build_payload(range_1, range_2)
  791. expect(mock_range).to have_received(:overlaps?).exactly(1).times
  792. end
  793. it 'checks overlap 3 times for 3 ranges' do
  794. described_class.verify_date_range 'url', build_payload(range_1, range_2, range_3)
  795. expect(mock_range).to have_received(:overlaps?).exactly(3).times
  796. end
  797. end
  798. end
  799. describe '.verify_single_key_range' do
  800. let(:range_1) { DateTime.new(2020, 1, 1)..DateTime.new(2021, 12, 31) }
  801. let(:range_2) { DateTime.new(2020, 3, 1)..DateTime.new(2020, 3, 31) }
  802. let(:range_3) { DateTime.new(2018, 3, 1)..DateTime.new(2018, 3, 31) }
  803. it 'returns true with a single range' do
  804. result = described_class.verify_single_key_range [range_1]
  805. expect(result).to be_truthy
  806. end
  807. it 'returns true with overlapping ranges' do
  808. result = described_class.verify_single_key_range [range_1, range_2]
  809. expect(result).to be_truthy
  810. end
  811. it 'returns false with non-overlapping ranges' do
  812. result = described_class.verify_single_key_range [range_1, range_3]
  813. expect(result).to be_falsey
  814. end
  815. end
  816. describe '.convert_es_date_range' do
  817. let(:from) { DateTime.new 2018, 1, 1, 17 }
  818. let(:from_placeholder) { DateTime.new(-9999, 1, 1) }
  819. let(:to) { DateTime.new 2020, 10, 1, 23 }
  820. let(:to_placeholder) { DateTime.new 9999, 1, 1 }
  821. it 'converts range' do
  822. result = described_class.convert_es_date_range(
  823. {
  824. range: {
  825. created_at: {
  826. from: '2018-01-01T17:00:00.000Z',
  827. to: '2020-10-01T23:00:00Z'
  828. }
  829. }
  830. }
  831. )
  832. expect(result).to eq from..to
  833. end
  834. it 'converts less than' do
  835. result = described_class.convert_es_date_range(
  836. {
  837. range: {
  838. created_at: {
  839. lt: '2020-10-01T23:00:00Z'
  840. }
  841. }
  842. }
  843. )
  844. expect(result).to eq from_placeholder..to
  845. end
  846. it 'converts greater than' do
  847. result = described_class.convert_es_date_range(
  848. {
  849. range: {
  850. created_at: {
  851. gt: '2018-01-01T17:00:00.000Z',
  852. }
  853. }
  854. }
  855. )
  856. expect(result).to eq from..to_placeholder
  857. end
  858. end
  859. end