ticket_spec.rb 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948
  1. require 'rails_helper'
  2. require 'models/application_model_examples'
  3. require 'models/concerns/can_be_imported_examples'
  4. require 'models/concerns/can_csv_import_examples'
  5. require 'models/concerns/has_history_examples'
  6. require 'models/concerns/has_tags_examples'
  7. require 'models/concerns/has_taskbars_examples'
  8. require 'models/concerns/has_xss_sanitized_note_examples'
  9. require 'models/concerns/has_object_manager_attributes_validation_examples'
  10. require 'models/tag/writes_to_ticket_history_examples'
  11. require 'models/ticket/calls_stats_ticket_reopen_log_examples'
  12. require 'models/ticket/enqueues_user_ticket_counter_job_examples'
  13. require 'models/ticket/escalation_examples'
  14. require 'models/ticket/resets_pending_time_seconds_examples'
  15. require 'models/ticket/sets_close_time_examples'
  16. require 'models/ticket/sets_last_owner_update_time_examples'
  17. RSpec.describe Ticket, type: :model do
  18. subject(:ticket) { create(:ticket) }
  19. it_behaves_like 'ApplicationModel'
  20. it_behaves_like 'CanBeImported'
  21. it_behaves_like 'CanCsvImport'
  22. it_behaves_like 'HasHistory', history_relation_object: ['Ticket::Article', 'Mention']
  23. it_behaves_like 'HasTags'
  24. it_behaves_like 'TagWritesToTicketHistory'
  25. it_behaves_like 'HasTaskbars'
  26. it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
  27. it_behaves_like 'HasObjectManagerAttributesValidation'
  28. it_behaves_like 'Ticket::Escalation'
  29. it_behaves_like 'TicketCallsStatsTicketReopenLog'
  30. it_behaves_like 'TicketEnqueuesTicketUserTicketCounterJob'
  31. it_behaves_like 'TicketResetsPendingTimeSeconds'
  32. it_behaves_like 'TicketSetsCloseTime'
  33. it_behaves_like 'TicketSetsLastOwnerUpdateTime'
  34. describe 'Class methods:' do
  35. describe '.selectors' do
  36. # https://github.com/zammad/zammad/issues/1769
  37. context 'when matching multiple tickets, each with multiple articles' do
  38. let(:tickets) { create_list(:ticket, 2) }
  39. before do
  40. create(:ticket_article, ticket: tickets.first, from: 'asdf1@blubselector.de')
  41. create(:ticket_article, ticket: tickets.first, from: 'asdf2@blubselector.de')
  42. create(:ticket_article, ticket: tickets.first, from: 'asdf3@blubselector.de')
  43. create(:ticket_article, ticket: tickets.last, from: 'asdf4@blubselector.de')
  44. create(:ticket_article, ticket: tickets.last, from: 'asdf5@blubselector.de')
  45. create(:ticket_article, ticket: tickets.last, from: 'asdf6@blubselector.de')
  46. end
  47. let(:condition) do
  48. {
  49. 'article.from' => {
  50. operator: 'contains',
  51. value: 'blubselector.de',
  52. },
  53. }
  54. end
  55. it 'returns a list of unique tickets (i.e., no duplicates)' do
  56. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  57. .to match_array([2, tickets.to_a])
  58. end
  59. end
  60. end
  61. end
  62. describe 'Instance methods:' do
  63. describe '#merge_to' do
  64. let(:target_ticket) { create(:ticket) }
  65. context 'when source ticket has Links' do
  66. let(:linked_tickets) { create_list(:ticket, 3) }
  67. let(:links) { linked_tickets.map { |l| create(:link, from: ticket, to: l) } }
  68. it 'reassigns all links to the target ticket after merge' do
  69. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  70. .to change { links.each(&:reload).map(&:link_object_source_value) }
  71. .to(Array.new(3) { target_ticket.id })
  72. end
  73. end
  74. context 'when attempting to cross-merge (i.e., to merge B → A after merging A → B)' do
  75. before { target_ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  76. it 'raises an error' do
  77. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  78. .to raise_error('ticket already merged, no merge into merged ticket possible')
  79. end
  80. end
  81. context 'when attempting to self-merge (i.e., to merge A → A)' do
  82. it 'raises an error' do
  83. expect { ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  84. .to raise_error("Can't merge ticket with it self!")
  85. end
  86. end
  87. context 'when both tickets are linked with the same parent (parent->child)' do
  88. let(:parent) { create(:ticket) }
  89. before do
  90. create(:link,
  91. link_type: 'child',
  92. link_object_source_value: ticket.id,
  93. link_object_target_value: parent.id)
  94. create(:link,
  95. link_type: 'child',
  96. link_object_source_value: target_ticket.id,
  97. link_object_target_value: parent.id)
  98. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  99. end
  100. it 'does remove the link from the merged ticket' do
  101. links = Link.list(
  102. link_object: 'Ticket',
  103. link_object_value: ticket.id
  104. )
  105. expect(links.count).to eq(1) # one link to the source ticket (no parent link)
  106. end
  107. it 'does not remove the link from the target ticket' do
  108. links = Link.list(
  109. link_object: 'Ticket',
  110. link_object_value: target_ticket.id
  111. )
  112. expect(links.count).to eq(2) # one link to the merged ticket + parent link
  113. end
  114. end
  115. context 'when both tickets are linked with the same parent (child->parent)' do
  116. let(:parent) { create(:ticket) }
  117. before do
  118. create(:link,
  119. link_type: 'child',
  120. link_object_source_value: parent.id,
  121. link_object_target_value: ticket.id)
  122. create(:link,
  123. link_type: 'child',
  124. link_object_source_value: parent.id,
  125. link_object_target_value: target_ticket.id)
  126. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  127. end
  128. it 'does remove the link from the merged ticket' do
  129. links = Link.list(
  130. link_object: 'Ticket',
  131. link_object_value: ticket.id
  132. )
  133. expect(links.count).to eq(1) # one link to the source ticket (no parent link)
  134. end
  135. it 'does not remove the link from the target ticket' do
  136. links = Link.list(
  137. link_object: 'Ticket',
  138. link_object_value: target_ticket.id
  139. )
  140. expect(links.count).to eq(2) # one link to the merged ticket + parent link
  141. end
  142. end
  143. context 'when both tickets are linked with the same parent (different link types)' do
  144. let(:parent) { create(:ticket) }
  145. before do
  146. create(:link,
  147. link_type: 'normal',
  148. link_object_source_value: parent.id,
  149. link_object_target_value: ticket.id)
  150. create(:link,
  151. link_type: 'child',
  152. link_object_source_value: parent.id,
  153. link_object_target_value: target_ticket.id)
  154. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  155. end
  156. it 'does remove the link from the merged ticket' do
  157. links = Link.list(
  158. link_object: 'Ticket',
  159. link_object_value: ticket.id
  160. )
  161. expect(links.count).to eq(1) # one link to the source ticket (no normal link)
  162. end
  163. it 'does not remove the link from the target ticket' do
  164. links = Link.list(
  165. link_object: 'Ticket',
  166. link_object_value: target_ticket.id
  167. )
  168. expect(links.count).to eq(3) # one lin to the merged ticket + parent link + normal link
  169. end
  170. end
  171. context 'when both tickets having mentions to the same user' do
  172. let(:watcher) { create(:agent) }
  173. before do
  174. create(:mention, mentionable: ticket, user: watcher)
  175. create(:mention, mentionable: target_ticket, user: watcher)
  176. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  177. end
  178. it 'does remove the link from the merged ticket' do
  179. expect(target_ticket.mentions.count).to eq(1) # one mention to watcher user
  180. end
  181. end
  182. context 'when merging' do
  183. let(:merge_user) { create(:user) }
  184. before do
  185. # create target ticket early
  186. # to avoid a race condition
  187. # when creating the history entries
  188. target_ticket
  189. travel 5.minutes
  190. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  191. end
  192. # Issue #2469 - Add information "Ticket merged" to History
  193. it 'creates history entries in both the origin ticket and the target ticket' do
  194. expect(target_ticket.history_get.size).to eq 2
  195. target_history = target_ticket.history_get.last
  196. expect(target_history['object']).to eq 'Ticket'
  197. expect(target_history['type']).to eq 'received_merge'
  198. expect(target_history['created_by_id']).to eq merge_user.id
  199. expect(target_history['o_id']).to eq target_ticket.id
  200. expect(target_history['id_to']).to eq target_ticket.id
  201. expect(target_history['id_from']).to eq ticket.id
  202. expect(ticket.history_get.size).to eq 4
  203. origin_history = ticket.reload.history_get[1]
  204. expect(origin_history['object']).to eq 'Ticket'
  205. expect(origin_history['type']).to eq 'merged_into'
  206. expect(origin_history['created_by_id']).to eq merge_user.id
  207. expect(origin_history['o_id']).to eq ticket.id
  208. expect(origin_history['id_to']).to eq target_ticket.id
  209. expect(origin_history['id_from']).to eq ticket.id
  210. end
  211. it 'sends ExternalSync.migrate' do
  212. allow(ExternalSync).to receive(:migrate)
  213. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  214. expect(ExternalSync).to have_received(:migrate).with('Ticket', ticket.id, target_ticket.id)
  215. end
  216. # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
  217. context 'and deleting the origin ticket' do
  218. it 'adds reference number and title to the target ticket' do
  219. expect { ticket.destroy }
  220. .to change { target_ticket.history_get.find { |elem| elem.fetch('type') == 'received_merge' }['value_from'] }
  221. .to("##{ticket.number} #{ticket.title}")
  222. end
  223. end
  224. # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
  225. context 'and deleting the target ticket' do
  226. it 'adds reference number and title to the origin ticket' do
  227. expect { target_ticket.destroy }
  228. .to change { ticket.history_get.find { |elem| elem.fetch('type') == 'merged_into' }['value_to'] }
  229. .to("##{target_ticket.number} #{target_ticket.title}")
  230. end
  231. end
  232. end
  233. end
  234. describe '#perform_changes' do
  235. # a `performable` can be a Trigger or a Job
  236. # we use DuckTyping and expect that a performable
  237. # implements the following interface
  238. let(:performable) { OpenStruct.new(id: 1, perform: perform) }
  239. # Regression test for https://github.com/zammad/zammad/issues/2001
  240. describe 'argument handling' do
  241. let(:perform) do
  242. {
  243. 'notification.email' => {
  244. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  245. recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
  246. subject: "Autoclose (\#{ticket.title})"
  247. }
  248. }
  249. end
  250. it 'does not mutate contents of "perform" hash' do
  251. expect { ticket.perform_changes(performable, 'trigger', {}, 1) }
  252. .not_to change { perform }
  253. end
  254. end
  255. context 'with "ticket.state_id" key in "perform" hash' do
  256. let(:perform) do
  257. {
  258. 'ticket.state_id' => {
  259. 'value' => Ticket::State.lookup(name: 'closed').id
  260. }
  261. }
  262. end
  263. it 'changes #state to specified value' do
  264. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  265. .to change { ticket.reload.state.name }.to('closed')
  266. end
  267. end
  268. # Test for backwards compatibility after PR https://github.com/zammad/zammad/pull/2862
  269. context 'with "pending_time" => { "value": DATE } in "perform" hash' do
  270. let(:perform) do
  271. {
  272. 'ticket.state_id' => {
  273. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  274. },
  275. 'ticket.pending_time' => {
  276. 'value' => timestamp,
  277. },
  278. }
  279. end
  280. let(:timestamp) { Time.zone.now }
  281. it 'changes pending date to given date' do
  282. freeze_time do
  283. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  284. .to change(ticket, :pending_time).to(be_within(1.minute).of(timestamp))
  285. end
  286. end
  287. end
  288. # Test for PR https://github.com/zammad/zammad/pull/2862
  289. context 'with "pending_time" => { "operator": "relative" } in "perform" hash' do
  290. shared_examples 'verify' do
  291. it 'verify relative pending time rule' do
  292. freeze_time do
  293. interval = relative_value.send(relative_range).from_now
  294. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  295. .to change(ticket, :pending_time).to(be_within(1.minute).of(interval))
  296. end
  297. end
  298. end
  299. let(:perform) do
  300. {
  301. 'ticket.state_id' => {
  302. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  303. },
  304. 'ticket.pending_time' => {
  305. 'operator' => 'relative',
  306. 'value' => relative_value,
  307. 'range' => relative_range_config
  308. },
  309. }
  310. end
  311. let(:relative_range_config) { relative_range.to_s.singularize }
  312. context 'and value in days' do
  313. let(:relative_value) { 2 }
  314. let(:relative_range) { :days }
  315. include_examples 'verify'
  316. end
  317. context 'and value in minutes' do
  318. let(:relative_value) { 60 }
  319. let(:relative_range) { :minutes }
  320. include_examples 'verify'
  321. end
  322. end
  323. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  324. let(:perform) do
  325. {
  326. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  327. 'ticket.action' => { 'value' => 'delete' },
  328. }
  329. end
  330. it 'performs a ticket deletion on a ticket' do
  331. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  332. .to change(ticket, :destroyed?).to(true)
  333. end
  334. end
  335. context 'with a "notification.email" trigger' do
  336. # Regression test for https://github.com/zammad/zammad/issues/1543
  337. #
  338. # If a new article fires an email notification trigger,
  339. # and then another article is added to the same ticket
  340. # before that trigger is performed,
  341. # the email template's 'article' var should refer to the originating article,
  342. # not the newest one.
  343. #
  344. # (This occurs whenever one action fires multiple email notification triggers.)
  345. context 'when two articles are created before the trigger fires once (race condition)' do
  346. let!(:article) { create(:ticket_article, ticket: ticket) }
  347. let!(:new_article) { create(:ticket_article, ticket: ticket) }
  348. let(:trigger) do
  349. build(:trigger,
  350. perform: {
  351. 'notification.email' => {
  352. body: '',
  353. recipient: 'ticket_customer',
  354. subject: ''
  355. }
  356. })
  357. end
  358. # required by Ticket#perform_changes for email notifications
  359. before { article.ticket.group.update(email_address: create(:email_address)) }
  360. it 'passes the first article to NotificationFactory::Mailer' do
  361. expect(NotificationFactory::Mailer)
  362. .to receive(:template)
  363. .with(hash_including(objects: { ticket: ticket, article: article }))
  364. .at_least(:once)
  365. .and_call_original
  366. expect(NotificationFactory::Mailer)
  367. .not_to receive(:template)
  368. .with(hash_including(objects: { ticket: ticket, article: new_article }))
  369. ticket.perform_changes(trigger, 'trigger', { article_id: article.id }, 1)
  370. end
  371. end
  372. end
  373. context 'with a notification trigger' do
  374. # https://github.com/zammad/zammad/issues/2782
  375. #
  376. # Notification triggers should log notification as private or public
  377. # according to given configuration
  378. let(:user) { create(:admin, mobile: '+37061010000') }
  379. before { ticket.group.users << user }
  380. let(:perform) do
  381. {
  382. notification_key => {
  383. body: 'Old programmers never die. They just branch to a new address.',
  384. recipient: 'ticket_agents',
  385. subject: 'Old programmers never die. They just branch to a new address.'
  386. }
  387. }.deep_merge(additional_options).deep_stringify_keys
  388. end
  389. let(:notification_key) { "notification.#{notification_type}" }
  390. shared_examples 'verify log visibility status' do
  391. shared_examples 'notification trigger' do
  392. it 'adds Ticket::Article' do
  393. expect { ticket.perform_changes(performable, 'trigger', ticket, user) }
  394. .to change { ticket.articles.count }.by(1)
  395. end
  396. it 'new Ticket::Article visibility reflects setting' do
  397. ticket.perform_changes(performable, 'trigger', ticket, User.first)
  398. new_article = ticket.articles.reload.last
  399. expect(new_article.internal).to be target_internal_value
  400. end
  401. end
  402. context 'when set to private' do
  403. let(:additional_options) do
  404. {
  405. notification_key => {
  406. internal: true
  407. }
  408. }
  409. end
  410. let(:target_internal_value) { true }
  411. it_behaves_like 'notification trigger'
  412. end
  413. context 'when set to internal' do
  414. let(:additional_options) do
  415. {
  416. notification_key => {
  417. internal: false
  418. }
  419. }
  420. end
  421. let(:target_internal_value) { false }
  422. it_behaves_like 'notification trigger'
  423. end
  424. context 'when no selection was made' do # ensure previously created triggers default to public
  425. let(:additional_options) do
  426. {}
  427. end
  428. let(:target_internal_value) { false }
  429. it_behaves_like 'notification trigger'
  430. end
  431. end
  432. context 'dispatching email' do
  433. let(:notification_type) { :email }
  434. include_examples 'verify log visibility status'
  435. end
  436. context 'dispatching SMS' do
  437. let(:notification_type) { :sms }
  438. before { create(:channel, area: 'Sms::Notification') }
  439. include_examples 'verify log visibility status'
  440. end
  441. end
  442. context 'with a "notification.webhook" trigger', performs_jobs: true do
  443. let(:webhook) { create(:webhook, endpoint: 'http://api.example.com/webhook', signature_token: '53CR3t') }
  444. let(:trigger) do
  445. create(:trigger,
  446. perform: {
  447. 'notification.webhook' => { 'webhook_id' => webhook.id }
  448. })
  449. end
  450. it 'schedules the webhooks notification job' do
  451. expect { ticket.perform_changes(trigger, 'trigger', {}, 1) }.to have_enqueued_job(TriggerWebhookJob).with(trigger, ticket, nil)
  452. end
  453. end
  454. end
  455. describe '#trigger_based_notification?' do
  456. let(:ticket) { create(:ticket) }
  457. context 'with a normal user' do
  458. let(:customer) { create(:customer) }
  459. it 'send trigger base notification' do
  460. expect(ticket.send(:trigger_based_notification?, customer)).to eq(true)
  461. end
  462. end
  463. context 'with a permanent failed user' do
  464. let(:failed_date) { 1.second.ago }
  465. let(:customer) do
  466. user = create(:customer)
  467. user.preferences.merge!(mail_delivery_failed: true, mail_delivery_failed_data: failed_date)
  468. user.save!
  469. user
  470. end
  471. it 'send no trigger base notification' do
  472. expect(ticket.send(:trigger_based_notification?, customer)).to eq(false)
  473. end
  474. context 'with failed date 61 days ago' do
  475. let(:failed_date) { 61.days.ago }
  476. it 'send trigger base notification' do
  477. expect(ticket.send(:trigger_based_notification?, customer)).to eq(true)
  478. end
  479. end
  480. end
  481. end
  482. describe '#subject_build' do
  483. context 'with default "ticket_hook_position" setting ("right")' do
  484. it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
  485. expect(ticket.subject_build('foo'))
  486. .to eq("foo [Ticket##{ticket.number}]")
  487. end
  488. context 'and a non-default value for the "ticket_hook" setting' do
  489. before { Setting.set('ticket_hook', 'bar baz') }
  490. it 'replaces "Ticket#" with the new ticket hook' do
  491. expect(ticket.subject_build('foo'))
  492. .to eq("foo [bar baz#{ticket.number}]")
  493. end
  494. end
  495. context 'and a non-default value for the "ticket_hook_divider" setting' do
  496. before { Setting.set('ticket_hook_divider', ': ') }
  497. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  498. expect(ticket.subject_build('foo'))
  499. .to eq("foo [Ticket#: #{ticket.number}]")
  500. end
  501. end
  502. context 'when the given string already contains a ticket reference, but in the wrong place' do
  503. it 'moves the ticket reference to the end' do
  504. expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
  505. .to eq("foo [Ticket##{ticket.number}]")
  506. end
  507. end
  508. context 'when the given string already contains an alternately formatted ticket reference' do
  509. it 'reformats the ticket reference' do
  510. expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
  511. .to eq("foo [Ticket##{ticket.number}]")
  512. end
  513. end
  514. end
  515. context 'with alternate "ticket_hook_position" setting ("left")' do
  516. before { Setting.set('ticket_hook_position', 'left') }
  517. it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
  518. expect(ticket.subject_build('foo'))
  519. .to eq("[Ticket##{ticket.number}] foo")
  520. end
  521. context 'and a non-default value for the "ticket_hook" setting' do
  522. before { Setting.set('ticket_hook', 'bar baz') }
  523. it 'replaces "Ticket#" with the new ticket hook' do
  524. expect(ticket.subject_build('foo'))
  525. .to eq("[bar baz#{ticket.number}] foo")
  526. end
  527. end
  528. context 'and a non-default value for the "ticket_hook_divider" setting' do
  529. before { Setting.set('ticket_hook_divider', ': ') }
  530. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  531. expect(ticket.subject_build('foo'))
  532. .to eq("[Ticket#: #{ticket.number}] foo")
  533. end
  534. end
  535. context 'when the given string already contains a ticket reference, but in the wrong place' do
  536. it 'moves the ticket reference to the start' do
  537. expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
  538. .to eq("[Ticket##{ticket.number}] foo")
  539. end
  540. end
  541. context 'when the given string already contains an alternately formatted ticket reference' do
  542. it 'reformats the ticket reference' do
  543. expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
  544. .to eq("[Ticket##{ticket.number}] foo")
  545. end
  546. end
  547. end
  548. end
  549. describe '#last_original_update_at' do
  550. let(:result) { ticket.last_original_update_at }
  551. it 'returns initial customer enquiry time when customer contacted repeatedly' do
  552. ticket
  553. target = create(:ticket_article, :inbound_email, ticket: ticket)
  554. travel 10.minutes
  555. create(:ticket_article, :inbound_email, ticket: ticket)
  556. expect(result).to eq target.created_at
  557. end
  558. it 'returns agent contact time when customer did not respond to agent reach out' do
  559. ticket
  560. create(:ticket_article, :outbound_email, ticket: ticket)
  561. expect(result).to eq ticket.last_contact_agent_at
  562. end
  563. it 'returns nil if no customer response' do
  564. ticket
  565. expect(result).to be_nil
  566. end
  567. context 'with customer enquiry and agent response' do
  568. before do
  569. ticket
  570. create(:ticket_article, :inbound_email, ticket: ticket)
  571. travel 10.minutes
  572. create(:ticket_article, :outbound_email, ticket: ticket)
  573. travel 10.minutes
  574. end
  575. it 'returns last customer enquiry time when agent did not respond yet' do
  576. target = create(:ticket_article, :inbound_email, ticket: ticket)
  577. expect(result).to eq target.created_at
  578. end
  579. it 'returns agent response time when agent responded to customer enquiry' do
  580. expect(result).to eq ticket.last_contact_agent_at
  581. end
  582. end
  583. end
  584. end
  585. describe 'Attributes:' do
  586. describe '#owner' do
  587. let(:original_owner) { create(:agent, groups: [ticket.group]) }
  588. before { ticket.update(owner: original_owner) }
  589. context 'when assigned directly' do
  590. context 'to an active agent belonging to ticket.group' do
  591. let(:agent) { create(:agent, groups: [ticket.group]) }
  592. it 'can be set' do
  593. expect { ticket.update(owner: agent) }
  594. .to change { ticket.reload.owner }.to(agent)
  595. end
  596. end
  597. context 'to an agent not belonging to ticket.group' do
  598. let(:agent) { create(:agent, groups: [other_group]) }
  599. let(:other_group) { create(:group) }
  600. it 'resets to default user (id: 1) instead' do
  601. expect { ticket.update(owner: agent) }
  602. .to change { ticket.reload.owner }.to(User.first)
  603. end
  604. end
  605. context 'to an inactive agent' do
  606. let(:agent) { create(:agent, groups: [ticket.group], active: false) }
  607. it 'resets to default user (id: 1) instead' do
  608. expect { ticket.update(owner: agent) }
  609. .to change { ticket.reload.owner }.to(User.first)
  610. end
  611. end
  612. context 'to a non-agent' do
  613. let(:agent) { create(:customer, groups: [ticket.group]) }
  614. it 'resets to default user (id: 1) instead' do
  615. expect { ticket.update(owner: agent) }
  616. .to change { ticket.reload.owner }.to(User.first)
  617. end
  618. end
  619. end
  620. context 'when the ticket is updated for any other reason' do
  621. context 'if original owner is still an active agent belonging to ticket.group' do
  622. it 'does not change' do
  623. expect { create(:ticket_article, ticket: ticket) }
  624. .not_to change { ticket.reload.owner }
  625. end
  626. end
  627. context 'if original owner has left ticket.group' do
  628. before { original_owner.groups = [] }
  629. it 'resets to default user (id: 1)' do
  630. expect { create(:ticket_article, ticket: ticket) }
  631. .to change { ticket.reload.owner }.to(User.first)
  632. end
  633. end
  634. context 'if original owner has become inactive' do
  635. before { original_owner.update(active: false) }
  636. it 'resets to default user (id: 1)' do
  637. expect { create(:ticket_article, ticket: ticket) }
  638. .to change { ticket.reload.owner }.to(User.first)
  639. end
  640. end
  641. context 'if original owner has lost agent status' do
  642. before { original_owner.roles = [create(:role)] }
  643. it 'resets to default user (id: 1)' do
  644. expect { create(:ticket_article, ticket: ticket) }
  645. .to change { ticket.reload.owner }.to(User.first)
  646. end
  647. end
  648. context 'when the Ticket is closed' do
  649. before do
  650. ticket.update!(state: Ticket::State.lookup(name: 'closed'))
  651. end
  652. context 'if original owner is still an active agent belonging to ticket.group' do
  653. it 'does not change' do
  654. expect { create(:ticket_article, ticket: ticket) }
  655. .not_to change { ticket.reload.owner }
  656. end
  657. end
  658. context 'if original owner has left ticket.group' do
  659. before { original_owner.groups = [] }
  660. it 'does not change' do
  661. expect { create(:ticket_article, ticket: ticket) }
  662. .not_to change { ticket.reload.owner }
  663. end
  664. end
  665. context 'if original owner has become inactive' do
  666. before { original_owner.update(active: false) }
  667. it 'does not change' do
  668. expect { create(:ticket_article, ticket: ticket) }
  669. .not_to change { ticket.reload.owner }
  670. end
  671. end
  672. context 'if original owner has lost agent status' do
  673. before { original_owner.roles = [create(:role)] }
  674. it 'does not change' do
  675. expect { create(:ticket_article, ticket: ticket) }
  676. .not_to change { ticket.reload.owner }
  677. end
  678. end
  679. end
  680. end
  681. end
  682. describe '#state' do
  683. context 'when originally "new" (default)' do
  684. context 'and a customer article is added' do
  685. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
  686. it 'stays "new"' do
  687. expect { article }
  688. .not_to change { ticket.state.name }.from('new')
  689. end
  690. end
  691. context 'and a non-customer article is added' do
  692. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  693. it 'switches to "open"' do
  694. expect { article }
  695. .to change { ticket.reload.state.name }.from('new').to('open')
  696. end
  697. end
  698. end
  699. context 'when originally "closed"' do
  700. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  701. context 'when a non-customer article is added' do
  702. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  703. it 'stays "closed"' do
  704. expect { article }.not_to change { ticket.reload.state.name }
  705. end
  706. end
  707. end
  708. end
  709. describe '#pending_time' do
  710. subject(:ticket) { create(:ticket, pending_time: 2.days.from_now) }
  711. context 'when #state is updated to any non-"pending" value' do
  712. it 'is reset to nil' do
  713. expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
  714. .to change(ticket, :pending_time).to(nil)
  715. end
  716. end
  717. # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
  718. context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
  719. it 'does not prematurely raise within the callback (#reset_pending_time)' do
  720. expect { ticket.update!(state: nil) }
  721. .to raise_error(ActiveRecord::StatementInvalid)
  722. end
  723. end
  724. end
  725. describe '#escalation_at' do
  726. before { travel_to(Time.current) } # freeze time
  727. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  728. let(:calendar) { create(:calendar, :'24/7') }
  729. context 'with no SLAs in the system' do
  730. it 'defaults to nil' do
  731. expect(ticket.escalation_at).to be(nil)
  732. end
  733. end
  734. context 'with an SLA in the system' do
  735. before { sla } # create sla
  736. it 'is set based on SLA’s #first_response_time' do
  737. expect(ticket.reload.escalation_at.to_i)
  738. .to eq(1.hour.from_now.to_i)
  739. end
  740. context 'after first agent’s response' do
  741. before { ticket } # create ticket
  742. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  743. it 'is updated based on the SLA’s #close_escalation_at' do
  744. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  745. expect { article }
  746. .to change { ticket.reload.escalation_at }
  747. .to(ticket.reload.close_escalation_at)
  748. end
  749. context 'when new #update_time is later than original #solution_time' do
  750. it 'is updated based on the original #solution_time' do
  751. travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  752. expect { article }
  753. .to change { ticket.reload.escalation_at }
  754. .to(4.hours.after(ticket.created_at))
  755. end
  756. end
  757. end
  758. end
  759. context 'when updated after an SLA has been added to the system' do
  760. before do
  761. ticket # create ticket
  762. sla # create sla
  763. end
  764. it 'is updated based on the new SLA’s #first_response_time' do
  765. expect { ticket.save! }
  766. .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
  767. end
  768. end
  769. context 'when updated after all SLAs have been removed from the system' do
  770. before do
  771. sla # create sla
  772. ticket # create ticket
  773. sla.destroy
  774. end
  775. it 'is set to nil' do
  776. expect { ticket.save! }
  777. .to change { ticket.reload.escalation_at }.to(nil)
  778. end
  779. end
  780. context 'when within last (relative)' do
  781. let(:first_response_time) { 5 }
  782. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  783. let(:within_condition) do
  784. { 'ticket.escalation_at'=>{ 'operator' => 'within last (relative)', 'value' => '30', 'range' => 'minute' } }
  785. end
  786. before do
  787. sla
  788. travel_to '2020-11-05 11:37:00'
  789. ticket = create(:ticket)
  790. create(:ticket_article, :inbound_email, ticket: ticket)
  791. travel_to '2020-11-05 11:50:00'
  792. end
  793. context 'when in range' do
  794. it 'does find the ticket' do
  795. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  796. expect(count).to eq(1)
  797. end
  798. end
  799. context 'when out of range' do
  800. let(:first_response_time) { 500 }
  801. it 'does not find the ticket' do
  802. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  803. expect(count).to eq(0)
  804. end
  805. end
  806. end
  807. context 'when within next (relative)' do
  808. let(:first_response_time) { 5 }
  809. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  810. let(:within_condition) do
  811. { 'ticket.escalation_at'=>{ 'operator' => 'within next (relative)', 'value' => '30', 'range' => 'minute' } }
  812. end
  813. before do
  814. sla
  815. travel_to '2020-11-05 11:50:00'
  816. ticket = create(:ticket)
  817. create(:ticket_article, :inbound_email, ticket: ticket)
  818. travel_to '2020-11-05 11:37:00'
  819. end
  820. context 'when in range' do
  821. it 'does find the ticket' do
  822. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  823. expect(count).to eq(1)
  824. end
  825. end
  826. context 'when out of range' do
  827. let(:first_response_time) { 500 }
  828. it 'does not find the ticket' do
  829. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  830. expect(count).to eq(0)
  831. end
  832. end
  833. end
  834. end
  835. describe '#first_response_escalation_at' do
  836. before { travel_to(Time.current) } # freeze time
  837. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  838. let(:calendar) { create(:calendar, :'24/7') }
  839. context 'with no SLAs in the system' do
  840. it 'defaults to nil' do
  841. expect(ticket.first_response_escalation_at).to be(nil)
  842. end
  843. end
  844. context 'with an SLA in the system' do
  845. before { sla } # create sla
  846. it 'is set based on SLA’s #first_response_time' do
  847. expect(ticket.reload.first_response_escalation_at.to_i)
  848. .to eq(1.hour.from_now.to_i)
  849. end
  850. context 'after first agent’s response' do
  851. before { ticket } # create ticket
  852. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  853. it 'is cleared' do
  854. expect { article }.to change { ticket.reload.first_response_escalation_at }.to(nil)
  855. end
  856. end
  857. end
  858. end
  859. describe '#update_escalation_at' do
  860. before { travel_to(Time.current) } # freeze time
  861. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  862. let(:calendar) { create(:calendar, :'24/7') }
  863. context 'with no SLAs in the system' do
  864. it 'defaults to nil' do
  865. expect(ticket.update_escalation_at).to be(nil)
  866. end
  867. end
  868. context 'with an SLA in the system' do
  869. before { sla } # create sla
  870. it 'is set based on SLA’s #update_time' do
  871. travel 1.minute
  872. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  873. expect(ticket.reload.update_escalation_at.to_i)
  874. .to eq(3.hours.from_now.to_i)
  875. end
  876. context 'after first agent’s response' do
  877. before { ticket } # create ticket
  878. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  879. it 'is updated based on the SLA’s #update_time' do
  880. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  881. travel(1.minute)
  882. expect { article }
  883. .to change { ticket.reload.update_escalation_at }
  884. .to(nil)
  885. end
  886. end
  887. end
  888. end
  889. describe '#close_escalation_at' do
  890. before { travel_to(Time.current) } # freeze time
  891. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  892. let(:calendar) { create(:calendar, :'24/7') }
  893. context 'with no SLAs in the system' do
  894. it 'defaults to nil' do
  895. expect(ticket.close_escalation_at).to be(nil)
  896. end
  897. end
  898. context 'with an SLA in the system' do
  899. before { sla } # create sla
  900. it 'is set based on SLA’s #solution_time' do
  901. expect(ticket.reload.close_escalation_at.to_i)
  902. .to eq(4.hours.from_now.to_i)
  903. end
  904. context 'after first agent’s response' do
  905. before { ticket } # create ticket
  906. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  907. it 'does not change' do
  908. expect { article }.not_to change(ticket, :close_escalation_at)
  909. end
  910. end
  911. end
  912. end
  913. end
  914. describe 'Associations:' do
  915. describe '#organization' do
  916. subject(:ticket) { build(:ticket, customer: customer, organization: nil) }
  917. let(:customer) { create(:customer, :with_org) }
  918. context 'on creation' do
  919. it 'automatically adopts the organization of its #customer' do
  920. expect { ticket.save }
  921. .to change(ticket, :organization).to(customer.organization)
  922. end
  923. end
  924. context 'on update of #customer.organization' do
  925. context 'to nil' do
  926. it 'automatically updates to #customer’s new value' do
  927. ticket.save
  928. expect { customer.update(organization: nil) }
  929. .to change { ticket.reload.organization }.to(nil)
  930. end
  931. end
  932. context 'to a different organization' do
  933. let(:new_org) { create(:organization) }
  934. it 'automatically updates to #customer’s new value' do
  935. ticket.save
  936. expect { customer.update(organization: new_org) }
  937. .to change { ticket.reload.organization }.to(new_org)
  938. end
  939. end
  940. end
  941. end
  942. end
  943. describe '.search' do
  944. shared_examples 'search permissions' do
  945. let(:group) { create(:group) }
  946. before do
  947. ticket
  948. end
  949. shared_examples 'permitted' do
  950. it 'finds Ticket' do
  951. expect( described_class.search(query: ticket.number, current_user: current_user).count ).to eq(1)
  952. end
  953. end
  954. shared_examples 'no permission' do
  955. it "doesn't find Ticket" do
  956. expect( described_class.search(query: ticket.number, current_user: current_user) ).to be_blank
  957. end
  958. end
  959. context 'Agent with Group access' do
  960. let(:ticket) do
  961. ticket = create(:ticket, group: group)
  962. create(:ticket_article, ticket: ticket)
  963. ticket
  964. end
  965. let(:current_user) { create(:agent, groups: [group]) }
  966. it_behaves_like 'permitted'
  967. end
  968. context 'when Agent is Customer of Ticket' do
  969. let(:ticket) do
  970. ticket = create(:ticket, customer: current_user)
  971. create(:ticket_article, ticket: ticket)
  972. ticket
  973. end
  974. let(:current_user) { create(:agent_and_customer) }
  975. it_behaves_like 'permitted'
  976. end
  977. context 'for Organization access' do
  978. let(:ticket) do
  979. ticket = create(:ticket, customer: customer)
  980. create(:ticket_article, ticket: ticket)
  981. ticket
  982. end
  983. let(:customer) { create(:customer, organization: organization) }
  984. context 'when Organization is shared' do
  985. let(:organization) { create(:organization, shared: true) }
  986. context 'for unrelated Agent' do
  987. let(:current_user) { create(:agent) }
  988. it_behaves_like 'no permission'
  989. end
  990. context 'for Agent in same Organization' do
  991. let(:current_user) { create(:agent_and_customer, organization: organization) }
  992. it_behaves_like 'permitted'
  993. end
  994. context 'for Customer of Ticket' do
  995. let(:current_user) { customer }
  996. it_behaves_like 'permitted'
  997. end
  998. end
  999. context 'when Organization is not shared' do
  1000. let(:organization) { create(:organization, shared: false) }
  1001. context 'for unrelated Agent' do
  1002. let(:current_user) { create(:agent) }
  1003. it_behaves_like 'no permission'
  1004. end
  1005. context 'for Agent in same Organization' do
  1006. let(:current_user) { create(:agent_and_customer, organization: organization) }
  1007. it_behaves_like 'no permission'
  1008. end
  1009. context 'for Customer of Ticket' do
  1010. let(:current_user) { customer }
  1011. it_behaves_like 'permitted'
  1012. end
  1013. end
  1014. end
  1015. end
  1016. context 'with searchindex', searchindex: true do
  1017. include_examples 'search permissions' do
  1018. before do
  1019. configure_elasticsearch(required: true, rebuild: true)
  1020. end
  1021. end
  1022. end
  1023. context 'without searchindex' do
  1024. include_examples 'search permissions'
  1025. end
  1026. end
  1027. describe 'Callbacks & Observers -' do
  1028. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  1029. it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
  1030. expect { create(:ticket, title: "some title \u0000 123") }
  1031. .not_to raise_error
  1032. end
  1033. end
  1034. describe 'XSS protection:' do
  1035. subject(:ticket) { create(:ticket, title: title) }
  1036. let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
  1037. it 'does not sanitize title' do
  1038. expect(ticket.title).to eq(title)
  1039. end
  1040. end
  1041. describe 'Cti::CallerId syncing:' do
  1042. subject(:ticket) { build(:ticket) }
  1043. before { allow(Cti::CallerId).to receive(:build) }
  1044. it 'adds numbers in article bodies (via Cti::CallerId.build)' do
  1045. expect(Cti::CallerId).to receive(:build).with(ticket)
  1046. ticket.save
  1047. Observer::Transaction.commit
  1048. Scheduler.worker(true)
  1049. end
  1050. end
  1051. describe 'Touching associations on update:' do
  1052. subject(:ticket) { create(:ticket, customer: customer) }
  1053. let(:customer) { create(:customer, organization: organization) }
  1054. let(:organization) { create(:organization) }
  1055. let(:other_customer) { create(:customer, organization: other_organization) }
  1056. let(:other_organization) { create(:organization) }
  1057. context 'on creation' do
  1058. it 'touches its customer and his organization' do
  1059. expect { ticket }
  1060. .to change { customer.reload.updated_at }
  1061. .and change { organization.reload.updated_at }
  1062. end
  1063. end
  1064. context 'on destruction' do
  1065. before { ticket }
  1066. it 'touches its customer and his organization' do
  1067. expect { ticket.destroy }
  1068. .to change { customer.reload.updated_at }
  1069. .and change { organization.reload.updated_at }
  1070. end
  1071. end
  1072. context 'when customer association is changed' do
  1073. it 'touches both old and new customer, and their organizations' do
  1074. expect { ticket.update(customer: other_customer) }
  1075. .to change { customer.reload.updated_at }
  1076. .and change { organization.reload.updated_at }
  1077. .and change { other_customer.reload.updated_at }
  1078. .and change { other_organization.reload.updated_at }
  1079. end
  1080. end
  1081. end
  1082. describe 'Association & attachment management:' do
  1083. it 'deletes all related ActivityStreams on destroy' do
  1084. create_list(:activity_stream, 3, o: ticket)
  1085. expect { ticket.destroy }
  1086. .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1087. .to(false)
  1088. end
  1089. it 'deletes all related Links on destroy' do
  1090. create(:link, from: ticket, to: create(:ticket))
  1091. create(:link, from: create(:ticket), to: ticket)
  1092. create(:link, from: ticket, to: create(:ticket))
  1093. expect { ticket.destroy }
  1094. .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
  1095. .to(false)
  1096. end
  1097. it 'deletes all related Articles on destroy' do
  1098. create_list(:ticket_article, 3, ticket: ticket)
  1099. expect { ticket.destroy }
  1100. .to change { Ticket::Article.exists?(ticket: ticket) }
  1101. .to(false)
  1102. end
  1103. it 'deletes all related OnlineNotifications on destroy' do
  1104. create_list(:online_notification, 3, o: ticket)
  1105. expect { ticket.destroy }
  1106. .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
  1107. .to(false)
  1108. end
  1109. it 'deletes all related Tags on destroy' do
  1110. create_list(:tag, 3, o: ticket)
  1111. expect { ticket.destroy }
  1112. .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1113. .to(false)
  1114. end
  1115. it 'deletes all related Histories on destroy' do
  1116. create_list(:history, 3, o: ticket)
  1117. expect { ticket.destroy }
  1118. .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1119. .to(false)
  1120. end
  1121. it 'deletes all related Karma::ActivityLogs on destroy' do
  1122. create_list(:'karma/activity_log', 3, o: ticket)
  1123. expect { ticket.destroy }
  1124. .to change { Karma::ActivityLog.exists?(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1125. .to(false)
  1126. end
  1127. it 'deletes all related RecentViews on destroy' do
  1128. create_list(:recent_view, 3, o: ticket)
  1129. expect { ticket.destroy }
  1130. .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1131. .to(false)
  1132. end
  1133. it 'destroys all related dependencies' do
  1134. refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
  1135. 'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
  1136. 'Ticket::Flag' => { 'ticket_id'=>1 } }
  1137. ticket = create(:ticket)
  1138. article = create(:ticket_article, ticket: ticket)
  1139. accounting = create(:ticket_time_accounting, ticket: ticket)
  1140. flag = create(:ticket_flag, ticket: ticket)
  1141. refs_ticket = Models.references('Ticket', ticket.id, true)
  1142. expect(refs_ticket).to eq(refs_known)
  1143. ticket.destroy
  1144. expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1145. expect { article.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1146. expect { accounting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1147. expect { flag.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1148. end
  1149. context 'when ticket is generated from email (with attachments)' do
  1150. subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
  1151. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail001.box')) }
  1152. it 'adds attachments to the Store{::File,::Provider::DB} tables' do
  1153. expect { ticket }
  1154. .to change(Store, :count).by(2)
  1155. .and change { Store::File.count }.by(2)
  1156. .and change { Store::Provider::DB.count }.by(2)
  1157. end
  1158. context 'and subsequently destroyed' do
  1159. it 'deletes all related attachments' do
  1160. ticket # create ticket
  1161. expect { ticket.destroy }
  1162. .to change(Store, :count).by(-2)
  1163. .and change { Store::File.count }.by(-2)
  1164. .and change { Store::Provider::DB.count }.by(-2)
  1165. end
  1166. end
  1167. context 'and a duplicate ticket is generated from the same email' do
  1168. before { ticket } # create ticket
  1169. let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
  1170. it 'adds duplicate attachments to the Store table only' do
  1171. expect { duplicate }
  1172. .to change(Store, :count).by(2)
  1173. .and change { Store::File.count }.by(0)
  1174. .and change { Store::Provider::DB.count }.by(0)
  1175. end
  1176. context 'when only the duplicate ticket is destroyed' do
  1177. it 'deletes only the duplicate attachments' do
  1178. duplicate # create ticket
  1179. expect { duplicate.destroy }
  1180. .to change(Store, :count).by(-2)
  1181. .and change { Store::File.count }.by(0)
  1182. .and change { Store::Provider::DB.count }.by(0)
  1183. end
  1184. it 'deletes all related attachments' do
  1185. duplicate.destroy
  1186. expect { ticket.destroy }
  1187. .to change(Store, :count).by(-2)
  1188. .and change { Store::File.count }.by(-2)
  1189. .and change { Store::Provider::DB.count }.by(-2)
  1190. end
  1191. end
  1192. end
  1193. end
  1194. end
  1195. describe 'Ticket lifecycle order-of-operations:' do
  1196. subject!(:ticket) { create(:ticket) }
  1197. let!(:agent) { create(:agent, groups: [group]) }
  1198. let(:group) { create(:group) }
  1199. before do
  1200. create(
  1201. :trigger,
  1202. condition: { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } },
  1203. perform: { 'ticket.group_id' => { 'value' => group.id } }
  1204. )
  1205. end
  1206. it 'fires triggers before new ticket notifications are sent' do
  1207. expect { Observer::Transaction.commit }
  1208. .to change { ticket.reload.group }.to(group)
  1209. expect { Scheduler.worker(true) }
  1210. .to change { NotificationFactory::Mailer.already_sent?(ticket, agent, 'email') }.to(1)
  1211. end
  1212. end
  1213. end
  1214. describe 'Mentions:', sends_notification_emails: true do
  1215. context 'when notifications' do
  1216. let(:prefs_matrix_no_mentions) do
  1217. { 'notification_config' =>
  1218. { 'matrix' =>
  1219. { 'create' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1220. 'update' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1221. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1222. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1223. end
  1224. let(:prefs_matrix_only_mentions) do
  1225. { 'notification_config' =>
  1226. { 'matrix' =>
  1227. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1228. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1229. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1230. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1231. end
  1232. let(:prefs_matrix_only_mentions_groups) do
  1233. { 'notification_config' =>
  1234. { 'matrix' =>
  1235. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1236. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1237. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1238. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } },
  1239. 'group_ids' => [create(:group).id, create(:group).id, create(:group).id] } }
  1240. end
  1241. let(:mention_group) { create(:group) }
  1242. let(:no_access_group) { create(:group) }
  1243. let(:user_only_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions) }
  1244. let(:user_read_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions_groups) }
  1245. let(:user_no_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_no_mentions) }
  1246. let(:ticket) { create(:ticket, group: mention_group, owner: user_no_mentions) }
  1247. it 'does inform mention user about the ticket update' do
  1248. create(:mention, mentionable: ticket, user: user_only_mentions)
  1249. create(:mention, mentionable: ticket, user: user_read_mentions)
  1250. create(:mention, mentionable: ticket, user: user_no_mentions)
  1251. Observer::Transaction.commit
  1252. Scheduler.worker(true)
  1253. check_notification do
  1254. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1255. Observer::Transaction.commit
  1256. Scheduler.worker(true)
  1257. sent(
  1258. template: 'ticket_update',
  1259. user: user_no_mentions,
  1260. )
  1261. sent(
  1262. template: 'ticket_update',
  1263. user: user_read_mentions,
  1264. )
  1265. sent(
  1266. template: 'ticket_update',
  1267. user: user_only_mentions,
  1268. )
  1269. end
  1270. end
  1271. it 'does not inform mention user about the ticket update' do
  1272. ticket
  1273. Observer::Transaction.commit
  1274. Scheduler.worker(true)
  1275. check_notification do
  1276. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1277. Observer::Transaction.commit
  1278. Scheduler.worker(true)
  1279. sent(
  1280. template: 'ticket_update',
  1281. user: user_no_mentions,
  1282. )
  1283. not_sent(
  1284. template: 'ticket_update',
  1285. user: user_read_mentions,
  1286. )
  1287. not_sent(
  1288. template: 'ticket_update',
  1289. user: user_only_mentions,
  1290. )
  1291. end
  1292. end
  1293. it 'does inform mention user about ticket creation' do
  1294. check_notification do
  1295. ticket = create(:ticket, owner: user_no_mentions, group: mention_group)
  1296. create(:mention, mentionable: ticket, user: user_read_mentions)
  1297. create(:mention, mentionable: ticket, user: user_only_mentions)
  1298. Observer::Transaction.commit
  1299. Scheduler.worker(true)
  1300. sent(
  1301. template: 'ticket_create',
  1302. user: user_no_mentions,
  1303. )
  1304. sent(
  1305. template: 'ticket_create',
  1306. user: user_read_mentions,
  1307. )
  1308. sent(
  1309. template: 'ticket_create',
  1310. user: user_only_mentions,
  1311. )
  1312. end
  1313. end
  1314. it 'does not inform mention user about ticket creation' do
  1315. check_notification do
  1316. create(:ticket, owner: user_no_mentions, group: mention_group)
  1317. Observer::Transaction.commit
  1318. Scheduler.worker(true)
  1319. sent(
  1320. template: 'ticket_create',
  1321. user: user_no_mentions,
  1322. )
  1323. not_sent(
  1324. template: 'ticket_create',
  1325. user: user_read_mentions,
  1326. )
  1327. not_sent(
  1328. template: 'ticket_create',
  1329. user: user_only_mentions,
  1330. )
  1331. end
  1332. end
  1333. it 'does not inform mention user about ticket creation because of no permissions' do
  1334. check_notification do
  1335. ticket = create(:ticket, group: no_access_group)
  1336. create(:mention, mentionable: ticket, user: user_read_mentions)
  1337. create(:mention, mentionable: ticket, user: user_only_mentions)
  1338. Observer::Transaction.commit
  1339. Scheduler.worker(true)
  1340. not_sent(
  1341. template: 'ticket_create',
  1342. user: user_read_mentions,
  1343. )
  1344. not_sent(
  1345. template: 'ticket_create',
  1346. user: user_only_mentions,
  1347. )
  1348. end
  1349. end
  1350. end
  1351. context 'selectors' do
  1352. let(:mention_group) { create(:group) }
  1353. let(:ticket_mentions) { create(:ticket, group: mention_group) }
  1354. let(:ticket_normal) { create(:ticket, group: mention_group) }
  1355. let(:user_mentions) { create(:agent, groups: [mention_group]) }
  1356. let(:user_no_mentions) { create(:agent, groups: [mention_group]) }
  1357. before do
  1358. described_class.destroy_all
  1359. ticket_normal
  1360. user_no_mentions
  1361. create(:mention, mentionable: ticket_mentions, user: user_mentions)
  1362. end
  1363. it 'pre condition is not_set' do
  1364. condition = {
  1365. 'ticket.mention_user_ids' => {
  1366. pre_condition: 'not_set',
  1367. operator: 'is',
  1368. },
  1369. }
  1370. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1371. .to match_array([1, [ticket_normal].to_a])
  1372. end
  1373. it 'pre condition is not not_set' do
  1374. condition = {
  1375. 'ticket.mention_user_ids' => {
  1376. pre_condition: 'not_set',
  1377. operator: 'is not',
  1378. },
  1379. }
  1380. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1381. .to match_array([1, [ticket_mentions].to_a])
  1382. end
  1383. it 'pre condition is current_user.id' do
  1384. condition = {
  1385. 'ticket.mention_user_ids' => {
  1386. pre_condition: 'current_user.id',
  1387. operator: 'is',
  1388. },
  1389. }
  1390. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1391. .to match_array([1, [ticket_mentions].to_a])
  1392. end
  1393. it 'pre condition is not current_user.id' do
  1394. condition = {
  1395. 'ticket.mention_user_ids' => {
  1396. pre_condition: 'current_user.id',
  1397. operator: 'is not',
  1398. },
  1399. }
  1400. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1401. .to match_array([0, []])
  1402. end
  1403. it 'pre condition is specific' do
  1404. condition = {
  1405. 'ticket.mention_user_ids' => {
  1406. pre_condition: 'specific',
  1407. operator: 'is',
  1408. value: user_mentions.id
  1409. },
  1410. }
  1411. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1412. .to match_array([1, [ticket_mentions].to_a])
  1413. end
  1414. it 'pre condition is not specific' do
  1415. condition = {
  1416. 'ticket.mention_user_ids' => {
  1417. pre_condition: 'specific',
  1418. operator: 'is not',
  1419. value: user_mentions.id
  1420. },
  1421. }
  1422. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1423. .to match_array([0, []])
  1424. end
  1425. end
  1426. end
  1427. describe '.search_index_attribute_lookup_oversized?' do
  1428. subject!(:ticket) { create(:ticket) }
  1429. context 'when payload is ok' do
  1430. let(:current_payload_size) { 3.megabyte }
  1431. it 'return false' do
  1432. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to eq false
  1433. end
  1434. end
  1435. context 'when payload is bigger' do
  1436. let(:current_payload_size) { 350.megabyte }
  1437. it 'return true' do
  1438. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to eq true
  1439. end
  1440. end
  1441. end
  1442. describe '.search_index_attribute_lookup_file_oversized?' do
  1443. subject!(:store) do
  1444. Store.add(
  1445. object: 'SomeObject',
  1446. o_id: 1,
  1447. data: (1024**800_000).to_s, # with 2.4 mb
  1448. filename: 'test.TXT',
  1449. created_by_id: 1,
  1450. )
  1451. end
  1452. context 'when total payload is ok' do
  1453. let(:current_payload_size) { 200.megabyte }
  1454. it 'return false' do
  1455. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to eq false
  1456. end
  1457. end
  1458. context 'when total payload is oversized' do
  1459. let(:current_payload_size) { 299.megabyte }
  1460. it 'return true' do
  1461. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to eq true
  1462. end
  1463. end
  1464. end
  1465. describe '.search_index_attribute_lookup_file_ignored?' do
  1466. context 'when attachment is indexable' do
  1467. subject!(:store_with_indexable_extention) do
  1468. Store.add(
  1469. object: 'SomeObject',
  1470. o_id: 1,
  1471. data: 'some content',
  1472. filename: 'test.TXT',
  1473. created_by_id: 1,
  1474. )
  1475. end
  1476. it 'return false' do
  1477. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_with_indexable_extention)).to eq false
  1478. end
  1479. end
  1480. context 'when attachment is no indexable' do
  1481. subject!(:store_without_indexable_extention) do
  1482. Store.add(
  1483. object: 'SomeObject',
  1484. o_id: 1,
  1485. data: 'some content',
  1486. filename: 'test.BIN',
  1487. created_by_id: 1,
  1488. )
  1489. end
  1490. it 'return true' do
  1491. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_without_indexable_extention)).to eq true
  1492. end
  1493. end
  1494. end
  1495. describe '.search_index_attribute_lookup' do
  1496. subject!(:ticket) { create(:ticket) }
  1497. let(:search_index_attribute_lookup) do
  1498. article1 = create(:ticket_article, ticket: ticket)
  1499. Store.add(
  1500. object: 'Ticket::Article',
  1501. o_id: article1.id,
  1502. data: 'some content',
  1503. filename: 'some_file.bin',
  1504. preferences: {
  1505. 'Content-Type' => 'text/plain',
  1506. },
  1507. created_by_id: 1,
  1508. )
  1509. Store.add(
  1510. object: 'Ticket::Article',
  1511. o_id: article1.id,
  1512. data: (1024**800_000).to_s, # with 2.4 mb
  1513. filename: 'some_file.pdf',
  1514. preferences: {
  1515. 'Content-Type' => 'image/pdf',
  1516. },
  1517. created_by_id: 1,
  1518. )
  1519. Store.add(
  1520. object: 'Ticket::Article',
  1521. o_id: article1.id,
  1522. data: (1024**2_000_000).to_s, # with 5,8 mb
  1523. filename: 'some_file.txt',
  1524. preferences: {
  1525. 'Content-Type' => 'text/plain',
  1526. },
  1527. created_by_id: 1,
  1528. )
  1529. create(:ticket_article, ticket: ticket, body: (1024**400_000).to_s.split(/(.{100})/).join(' ')) # body with 1,2 mb
  1530. create(:ticket_article, ticket: ticket)
  1531. ticket.search_index_attribute_lookup
  1532. end
  1533. context 'when es_attachment_max_size_in_mb takes all attachments' do
  1534. before { Setting.set('es_attachment_max_size_in_mb', 15) }
  1535. it 'verify count of articles' do
  1536. expect(search_index_attribute_lookup['article'].count).to eq 3
  1537. end
  1538. it 'verify count of attachments' do
  1539. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 2
  1540. end
  1541. it 'verify if pdf exists' do
  1542. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1543. end
  1544. it 'verify if txt exists' do
  1545. expect(search_index_attribute_lookup['article'][0]['attachment'][1]['_name']).to eq 'some_file.txt'
  1546. end
  1547. end
  1548. context 'when es_attachment_max_size_in_mb takes only one attachment' do
  1549. before { Setting.set('es_attachment_max_size_in_mb', 4) }
  1550. it 'verify count of articles' do
  1551. expect(search_index_attribute_lookup['article'].count).to eq 3
  1552. end
  1553. it 'verify count of attachments' do
  1554. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 1
  1555. end
  1556. it 'verify if pdf exists' do
  1557. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1558. end
  1559. end
  1560. context 'when es_attachment_max_size_in_mb takes no attachment' do
  1561. before { Setting.set('es_attachment_max_size_in_mb', 2) }
  1562. it 'verify count of articles' do
  1563. expect(search_index_attribute_lookup['article'].count).to eq 3
  1564. end
  1565. it 'verify count of attachments' do
  1566. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1567. end
  1568. end
  1569. context 'when es_total_max_size_in_mb takes no attachment and no oversized article' do
  1570. before { Setting.set('es_total_max_size_in_mb', 1) }
  1571. it 'verify count of articles' do
  1572. expect(search_index_attribute_lookup['article'].count).to eq 2
  1573. end
  1574. it 'verify count of attachments' do
  1575. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1576. end
  1577. end
  1578. end
  1579. end