ticket_spec.rb 89 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'models/application_model_examples'
  4. require 'models/concerns/can_be_imported_examples'
  5. require 'models/concerns/can_csv_import_examples'
  6. require 'models/concerns/can_csv_import_ticket_examples'
  7. require 'models/concerns/checks_core_workflow_examples'
  8. require 'models/concerns/has_history_examples'
  9. require 'models/concerns/has_tags_examples'
  10. require 'models/concerns/has_taskbars_examples'
  11. require 'models/concerns/has_xss_sanitized_note_examples'
  12. require 'models/concerns/has_object_manager_attributes_examples'
  13. require 'models/tag/writes_to_ticket_history_examples'
  14. require 'models/ticket/calls_stats_ticket_reopen_log_examples'
  15. require 'models/ticket/enqueues_user_ticket_counter_job_examples'
  16. require 'models/ticket/escalation_examples'
  17. require 'models/ticket/resets_pending_time_seconds_examples'
  18. require 'models/ticket/sets_close_time_examples'
  19. require 'models/ticket/sets_last_owner_update_time_examples'
  20. RSpec.describe Ticket, type: :model do
  21. subject(:ticket) { create(:ticket) }
  22. it_behaves_like 'ApplicationModel'
  23. it_behaves_like 'CanBeImported'
  24. it_behaves_like 'CanCsvImport'
  25. include_examples 'CanCsvImport - Ticket specific tests'
  26. it_behaves_like 'ChecksCoreWorkflow'
  27. it_behaves_like 'HasHistory', history_relation_object: ['Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom']
  28. it_behaves_like 'HasTags'
  29. it_behaves_like 'TagWritesToTicketHistory'
  30. it_behaves_like 'HasTaskbars'
  31. it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
  32. it_behaves_like 'HasObjectManagerAttributes'
  33. it_behaves_like 'Ticket::Escalation'
  34. it_behaves_like 'TicketCallsStatsTicketReopenLog'
  35. it_behaves_like 'TicketEnqueuesTicketUserTicketCounterJob'
  36. it_behaves_like 'TicketResetsPendingTimeSeconds'
  37. it_behaves_like 'TicketSetsCloseTime'
  38. it_behaves_like 'TicketSetsLastOwnerUpdateTime'
  39. describe 'Class methods:' do
  40. describe '.selectors' do
  41. # https://github.com/zammad/zammad/issues/1769
  42. context 'when matching multiple tickets, each with multiple articles' do
  43. let(:tickets) { create_list(:ticket, 2) }
  44. before do
  45. create(:ticket_article, ticket: tickets.first, from: 'asdf1@blubselector.de')
  46. create(:ticket_article, ticket: tickets.first, from: 'asdf2@blubselector.de')
  47. create(:ticket_article, ticket: tickets.first, from: 'asdf3@blubselector.de')
  48. create(:ticket_article, ticket: tickets.last, from: 'asdf4@blubselector.de')
  49. create(:ticket_article, ticket: tickets.last, from: 'asdf5@blubselector.de')
  50. create(:ticket_article, ticket: tickets.last, from: 'asdf6@blubselector.de')
  51. end
  52. let(:condition) do
  53. {
  54. 'article.from' => {
  55. operator: 'contains',
  56. value: 'blubselector.de',
  57. },
  58. }
  59. end
  60. it 'returns a list of unique tickets (i.e., no duplicates)' do
  61. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  62. .to contain_exactly(2, tickets.to_a)
  63. end
  64. end
  65. context 'when customer has multiple organizations' do
  66. let(:organization1) { create(:organization) }
  67. let(:organization2) { create(:organization) }
  68. let(:organization3) { create(:organization) }
  69. let(:customer) { create(:customer, organization: organization1, organizations: [organization2, organization3]) }
  70. let(:ticket1) { create(:ticket, customer: customer, organization: organization1) }
  71. let(:ticket2) { create(:ticket, customer: customer, organization: organization2) }
  72. let(:ticket3) { create(:ticket, customer: customer, organization: organization3) }
  73. before do
  74. ticket1 && ticket2 && ticket3
  75. end
  76. context 'when current user organization is used' do
  77. let(:condition) do
  78. {
  79. 'ticket.organization_id' => {
  80. operator: 'is', # is not
  81. pre_condition: 'current_user.organization_id',
  82. },
  83. }
  84. end
  85. it 'returns the customer tickets' do
  86. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: customer))
  87. .to contain_exactly(3, include(ticket1, ticket2, ticket3))
  88. end
  89. end
  90. end
  91. end
  92. end
  93. describe 'Instance methods:' do
  94. describe '#merge_to' do
  95. let(:target_ticket) { create(:ticket) }
  96. context 'when source ticket has Links' do
  97. let(:linked_tickets) { create_list(:ticket, 3) }
  98. let(:links) { linked_tickets.map { |l| create(:link, from: ticket, to: l) } }
  99. it 'reassigns all links to the target ticket after merge' do
  100. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  101. .to change { links.each(&:reload).map(&:link_object_source_value) }
  102. .to(Array.new(3) { target_ticket.id })
  103. end
  104. end
  105. context 'when attempting to cross-merge (i.e., to merge B → A after merging A → B)' do
  106. before { target_ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  107. it 'raises an error' do
  108. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  109. .to raise_error('It is not possible to merge into an already merged ticket.')
  110. end
  111. end
  112. context 'when attempting to self-merge (i.e., to merge A → A)' do
  113. it 'raises an error' do
  114. expect { ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  115. .to raise_error('A ticket cannot be merged into itself.')
  116. end
  117. end
  118. context 'when both tickets are linked with the same parent (parent->child)' do
  119. let(:parent) { create(:ticket) }
  120. before do
  121. create(:link,
  122. link_type: 'child',
  123. link_object_source_value: ticket.id,
  124. link_object_target_value: parent.id)
  125. create(:link,
  126. link_type: 'child',
  127. link_object_source_value: target_ticket.id,
  128. link_object_target_value: parent.id)
  129. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  130. end
  131. it 'does remove the link from the merged ticket' do
  132. links = Link.list(
  133. link_object: 'Ticket',
  134. link_object_value: ticket.id
  135. )
  136. expect(links.count).to eq(1) # one link to the source ticket (no parent link)
  137. end
  138. it 'does not remove the link from the target ticket' do
  139. links = Link.list(
  140. link_object: 'Ticket',
  141. link_object_value: target_ticket.id
  142. )
  143. expect(links.count).to eq(2) # one link to the merged ticket + parent link
  144. end
  145. end
  146. context 'when both tickets are linked with the same parent (child->parent)' do
  147. let(:parent) { create(:ticket) }
  148. before do
  149. create(:link,
  150. link_type: 'child',
  151. link_object_source_value: parent.id,
  152. link_object_target_value: ticket.id)
  153. create(:link,
  154. link_type: 'child',
  155. link_object_source_value: parent.id,
  156. link_object_target_value: target_ticket.id)
  157. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  158. end
  159. it 'does remove the link from the merged ticket' do
  160. links = Link.list(
  161. link_object: 'Ticket',
  162. link_object_value: ticket.id
  163. )
  164. expect(links.count).to eq(1) # one link to the source ticket (no parent link)
  165. end
  166. it 'does not remove the link from the target ticket' do
  167. links = Link.list(
  168. link_object: 'Ticket',
  169. link_object_value: target_ticket.id
  170. )
  171. expect(links.count).to eq(2) # one link to the merged ticket + parent link
  172. end
  173. end
  174. context 'when both tickets are linked with the same parent (different link types)' do
  175. let(:parent) { create(:ticket) }
  176. before do
  177. create(:link,
  178. link_type: 'normal',
  179. link_object_source_value: parent.id,
  180. link_object_target_value: ticket.id)
  181. create(:link,
  182. link_type: 'child',
  183. link_object_source_value: parent.id,
  184. link_object_target_value: target_ticket.id)
  185. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  186. end
  187. it 'does remove the link from the merged ticket' do
  188. links = Link.list(
  189. link_object: 'Ticket',
  190. link_object_value: ticket.id
  191. )
  192. expect(links.count).to eq(1) # one link to the source ticket (no normal link)
  193. end
  194. it 'does not remove the link from the target ticket' do
  195. links = Link.list(
  196. link_object: 'Ticket',
  197. link_object_value: target_ticket.id
  198. )
  199. expect(links.count).to eq(3) # one lin to the merged ticket + parent link + normal link
  200. end
  201. end
  202. context 'when both tickets having mentions to the same user' do
  203. let(:watcher) { create(:agent, groups: [ticket.group, target_ticket.group]) }
  204. before do
  205. create(:mention, mentionable: ticket, user: watcher)
  206. create(:mention, mentionable: target_ticket, user: watcher)
  207. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  208. end
  209. it 'does remove the link from the merged ticket' do
  210. expect(target_ticket.mentions.count).to eq(1) # one mention to watcher user
  211. end
  212. end
  213. context 'when merging a ticket with mentioned user who has no access to the target ticket' do
  214. let(:watcher) { create(:agent, groups: [ticket.group]) }
  215. it 'does remove the link from the merged ticket' do
  216. create(:mention, mentionable: ticket, user: watcher)
  217. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  218. .to change { target_ticket.mentions.count }
  219. .to(1)
  220. end
  221. end
  222. context 'when merging' do
  223. let(:merge_user) { create(:user) }
  224. before do
  225. # create target ticket early
  226. # to avoid a race condition
  227. # when creating the history entries
  228. target_ticket
  229. travel 5.minutes
  230. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  231. end
  232. # Issue #2469 - Add information "Ticket merged" to History
  233. it 'creates history entries in both the origin ticket and the target ticket' do
  234. expect(target_ticket.history_get.size).to eq 2
  235. target_history = target_ticket.history_get.last
  236. expect(target_history['object']).to eq 'Ticket'
  237. expect(target_history['type']).to eq 'received_merge'
  238. expect(target_history['created_by_id']).to eq merge_user.id
  239. expect(target_history['o_id']).to eq target_ticket.id
  240. expect(target_history['id_to']).to eq target_ticket.id
  241. expect(target_history['id_from']).to eq ticket.id
  242. expect(ticket.history_get.size).to eq 4
  243. origin_history = ticket.reload.history_get[1]
  244. expect(origin_history['object']).to eq 'Ticket'
  245. expect(origin_history['type']).to eq 'merged_into'
  246. expect(origin_history['created_by_id']).to eq merge_user.id
  247. expect(origin_history['o_id']).to eq ticket.id
  248. expect(origin_history['id_to']).to eq target_ticket.id
  249. expect(origin_history['id_from']).to eq ticket.id
  250. end
  251. it 'sends ExternalSync.migrate' do
  252. allow(ExternalSync).to receive(:migrate)
  253. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  254. expect(ExternalSync).to have_received(:migrate).with('Ticket', ticket.id, target_ticket.id)
  255. end
  256. # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
  257. context 'and deleting the origin ticket' do
  258. it 'adds reference number and title to the target ticket' do
  259. expect { ticket.destroy }
  260. .to change { target_ticket.history_get.find { |elem| elem.fetch('type') == 'received_merge' }['value_from'] }
  261. .to("##{ticket.number} #{ticket.title}")
  262. end
  263. end
  264. # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
  265. context 'and deleting the target ticket' do
  266. it 'adds reference number and title to the origin ticket' do
  267. expect { target_ticket.destroy }
  268. .to change { ticket.history_get.find { |elem| elem.fetch('type') == 'merged_into' }['value_to'] }
  269. .to("##{target_ticket.number} #{target_ticket.title}")
  270. end
  271. end
  272. end
  273. # https://github.com/zammad/zammad/issues/3105
  274. context 'when merge actions triggers exist', :performs_jobs do
  275. before do
  276. ticket && target_ticket
  277. merged_into_trigger && received_merge_trigger && update_trigger
  278. allow_any_instance_of(described_class).to receive(:perform_changes) do |ticket, trigger|
  279. log << { ticket: ticket.id, trigger: trigger.id }
  280. end
  281. perform_enqueued_jobs do
  282. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  283. end
  284. end
  285. let(:merged_into_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update.merged_into') }
  286. let(:received_merge_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update.received_merge') }
  287. let(:update_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update') }
  288. let(:log) { [] }
  289. it 'merge_into triggered with source ticket' do
  290. expect(log).to include({ ticket: ticket.id, trigger: merged_into_trigger.id })
  291. end
  292. it 'received_merge not triggered with source ticket' do
  293. expect(log).not_to include({ ticket: ticket.id, trigger: received_merge_trigger.id })
  294. end
  295. it 'update not triggered with source ticket' do
  296. expect(log).not_to include({ ticket: ticket.id, trigger: update_trigger.id })
  297. end
  298. it 'merge_into not triggered with target ticket' do
  299. expect(log).not_to include({ ticket: target_ticket.id, trigger: merged_into_trigger.id })
  300. end
  301. it 'received_merge triggered with target ticket' do
  302. expect(log).to include({ ticket: target_ticket.id, trigger: received_merge_trigger.id })
  303. end
  304. it 'update not triggered with target ticket' do
  305. expect(log).not_to include({ ticket: target_ticket.id, trigger: update_trigger.id })
  306. end
  307. end
  308. # https://github.com/zammad/zammad/issues/3105
  309. context 'when user has notifications enabled', :performs_jobs do
  310. before do
  311. user
  312. allow(OnlineNotification).to receive(:add) do |**args|
  313. next if args[:object] != 'Ticket'
  314. log << { type: :online, event: args[:type], ticket_id: args[:o_id], user_id: args[:user_id] }
  315. end
  316. allow(NotificationFactory::Mailer).to receive(:notification) do |**args|
  317. log << { type: :email, event: args[:template], ticket_id: args[:objects][:ticket].id, user_id: args[:user].id }
  318. end
  319. perform_enqueued_jobs do
  320. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  321. end
  322. end
  323. let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
  324. let(:log) { [] }
  325. it 'merge_into notification sent with source ticket' do
  326. expect(log).to include({ type: :online, event: 'update.merged_into', ticket_id: ticket.id, user_id: user.id })
  327. end
  328. it 'received_merge notification not sent with source ticket' do
  329. expect(log).not_to include({ type: :online, event: 'update.received_merge', ticket_id: ticket.id, user_id: user.id })
  330. end
  331. it 'update notification not sent with source ticket' do
  332. expect(log).not_to include({ type: :online, event: 'update', ticket_id: ticket.id, user_id: user.id })
  333. end
  334. it 'merge_into notification not sent with target ticket' do
  335. expect(log).not_to include({ type: :online, event: 'update.merged_into', ticket_id: target_ticket.id, user_id: user.id })
  336. end
  337. it 'received_merge notification sent with target ticket' do
  338. expect(log).to include({ type: :online, event: 'update.received_merge', ticket_id: target_ticket.id, user_id: user.id })
  339. end
  340. it 'update notification not sent with target ticket' do
  341. expect(log).not_to include({ type: :online, event: 'update', ticket_id: target_ticket.id, user_id: user.id })
  342. end
  343. it 'merge_into email sent with source ticket' do
  344. expect(log).to include({ type: :email, event: 'ticket_update_merged_into', ticket_id: ticket.id, user_id: user.id })
  345. end
  346. it 'received_merge email not sent with source ticket' do
  347. expect(log).not_to include({ type: :email, event: 'ticket_update_received_merge', ticket_id: ticket.id, user_id: user.id })
  348. end
  349. it 'update email not sent with source ticket' do
  350. expect(log).not_to include({ type: :email, event: 'ticket_update', ticket_id: ticket.id, user_id: user.id })
  351. end
  352. it 'merge_into email not sent with target ticket' do
  353. expect(log).not_to include({ type: :email, event: 'ticket_update_merged_into', ticket_id: target_ticket.id, user_id: user.id })
  354. end
  355. it 'received_merge email sent with target ticket' do
  356. expect(log).to include({ type: :email, event: 'ticket_update_received_merge', ticket_id: target_ticket.id, user_id: user.id })
  357. end
  358. it 'update email not sent with target ticket' do
  359. expect(log).not_to include({ type: :email, event: 'ticket_update', ticket_id: target_ticket.id, user_id: user.id })
  360. end
  361. end
  362. # https://github.com/zammad/zammad/issues/3105
  363. context 'when sending notification email correct template', :performs_jobs do
  364. before do
  365. user
  366. allow(NotificationFactory::Mailer).to receive(:send) do |**args|
  367. log << args[:subject]
  368. end
  369. perform_enqueued_jobs do
  370. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  371. end
  372. end
  373. let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
  374. let(:log) { [] }
  375. it 'is used for merged_into' do
  376. expect(log).to include(start_with("Ticket (#{ticket.title}) was merged into another ticket"))
  377. end
  378. it 'is used for received_merge' do
  379. expect(log).to include(start_with("Another ticket was merged into ticket (#{target_ticket.title})"))
  380. end
  381. end
  382. context 'ApplicationHandleInfo context' do
  383. it 'gets switched to "merge"' do
  384. allow(ApplicationHandleInfo).to receive('context=')
  385. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  386. expect(ApplicationHandleInfo).to have_received('context=').with('merge').at_least(1)
  387. end
  388. it 'reverts back to default' do
  389. allow(ApplicationHandleInfo).to receive('context=')
  390. ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
  391. expect(ApplicationHandleInfo.context).not_to eq 'merge'
  392. end
  393. end
  394. end
  395. describe '#perform_changes' do
  396. before do
  397. stub_const('PERFORMABLE_STRUCT', Struct.new(:id, :perform, keyword_init: true))
  398. end
  399. # a `performable` can be a Trigger or a Job
  400. # we use DuckTyping and expect that a performable
  401. # implements the following interface
  402. let(:performable) do
  403. PERFORMABLE_STRUCT.new(id: 1, perform: perform)
  404. end
  405. # Regression test for https://github.com/zammad/zammad/issues/2001
  406. describe 'argument handling' do
  407. let(:perform) do
  408. {
  409. 'notification.email' => {
  410. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  411. recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
  412. subject: "Autoclose (\#{ticket.title})"
  413. }
  414. }
  415. end
  416. it 'does not mutate contents of "perform" hash' do
  417. expect { ticket.perform_changes(performable, 'trigger', {}, 1) }
  418. .not_to change { perform }
  419. end
  420. end
  421. context 'with "ticket.state_id" key in "perform" hash' do
  422. let(:perform) do
  423. {
  424. 'ticket.state_id' => {
  425. 'value' => Ticket::State.lookup(name: 'closed').id
  426. }
  427. }
  428. end
  429. it 'changes #state to specified value' do
  430. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  431. .to change { ticket.reload.state.name }.to('closed')
  432. end
  433. end
  434. # Test for backwards compatibility after PR https://github.com/zammad/zammad/pull/2862
  435. context 'with "pending_time" => { "value": DATE } in "perform" hash' do
  436. let(:perform) do
  437. {
  438. 'ticket.state_id' => {
  439. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  440. },
  441. 'ticket.pending_time' => {
  442. 'value' => timestamp,
  443. },
  444. }
  445. end
  446. let(:timestamp) { Time.zone.now }
  447. it 'changes pending date to given date' do
  448. freeze_time do
  449. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  450. .to change(ticket, :pending_time).to(be_within(1.minute).of(timestamp))
  451. end
  452. end
  453. end
  454. # Test for PR https://github.com/zammad/zammad/pull/2862
  455. context 'with "pending_time" => { "operator": "relative" } in "perform" hash' do
  456. shared_examples 'verify' do
  457. it 'verify relative pending time rule' do
  458. freeze_time do
  459. interval = relative_value.send(relative_range).from_now
  460. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  461. .to change(ticket, :pending_time).to(be_within(1.minute).of(interval))
  462. end
  463. end
  464. end
  465. let(:perform) do
  466. {
  467. 'ticket.state_id' => {
  468. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  469. },
  470. 'ticket.pending_time' => {
  471. 'operator' => 'relative',
  472. 'value' => relative_value,
  473. 'range' => relative_range_config
  474. },
  475. }
  476. end
  477. let(:relative_range_config) { relative_range.to_s.singularize }
  478. context 'and value in days' do
  479. let(:relative_value) { 2 }
  480. let(:relative_range) { :days }
  481. include_examples 'verify'
  482. end
  483. context 'and value in minutes' do
  484. let(:relative_value) { 60 }
  485. let(:relative_range) { :minutes }
  486. include_examples 'verify'
  487. end
  488. context 'and value in weeks' do
  489. let(:relative_value) { 2 }
  490. let(:relative_range) { :weeks }
  491. include_examples 'verify'
  492. end
  493. end
  494. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  495. let(:perform) do
  496. {
  497. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  498. 'ticket.action' => { 'value' => 'delete' },
  499. }
  500. end
  501. it 'performs a ticket deletion on a ticket' do
  502. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  503. .to change(ticket, :destroyed?).to(true)
  504. end
  505. end
  506. context 'with a "notification.email" trigger' do
  507. # Regression test for https://github.com/zammad/zammad/issues/1543
  508. #
  509. # If a new article fires an email notification trigger,
  510. # and then another article is added to the same ticket
  511. # before that trigger is performed,
  512. # the email template's 'article' var should refer to the originating article,
  513. # not the newest one.
  514. #
  515. # (This occurs whenever one action fires multiple email notification triggers.)
  516. context 'when two articles are created before the trigger fires once (race condition)' do
  517. let!(:article) { create(:ticket_article, ticket: ticket) }
  518. let!(:new_article) { create(:ticket_article, ticket: ticket) }
  519. let(:trigger) do
  520. build(:trigger,
  521. perform: {
  522. 'notification.email' => {
  523. body: '',
  524. recipient: 'ticket_customer',
  525. subject: ''
  526. }
  527. })
  528. end
  529. let(:objects) do
  530. last_article = nil
  531. last_internal_article = nil
  532. last_external_article = nil
  533. all_articles = ticket.articles
  534. if article.nil?
  535. last_article = all_articles.last
  536. last_internal_article = all_articles.reverse.find(&:internal?)
  537. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  538. else
  539. last_article = article
  540. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  541. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  542. end
  543. {
  544. ticket: ticket,
  545. article: last_article,
  546. last_article: last_article,
  547. last_internal_article: last_internal_article,
  548. last_external_article: last_external_article,
  549. created_article: article,
  550. created_internal_article: article&.internal? ? article : nil,
  551. created_external_article: article&.internal? ? nil : article,
  552. }
  553. end
  554. # required by Ticket#perform_changes for email notifications
  555. before { article.ticket.group.update(email_address: create(:email_address)) }
  556. it 'passes the first article to NotificationFactory::Mailer' do
  557. expect(NotificationFactory::Mailer)
  558. .to receive(:template)
  559. .with(hash_including(objects: objects))
  560. .at_least(:once)
  561. .and_call_original
  562. expect(NotificationFactory::Mailer)
  563. .not_to receive(:template)
  564. .with(hash_including(objects: { ticket: ticket, article: new_article }))
  565. ticket.perform_changes(trigger, 'trigger', { article_id: article.id }, 1)
  566. end
  567. end
  568. end
  569. context 'with a notification trigger' do
  570. # https://github.com/zammad/zammad/issues/2782
  571. #
  572. # Notification triggers should log notification as private or public
  573. # according to given configuration
  574. let(:user) { create(:admin, mobile: '+37061010000') }
  575. let(:item) do
  576. {
  577. object: 'Ticket',
  578. object_id: ticket.id,
  579. user_id: user.id,
  580. type: 'update',
  581. article_id: ticket_article.id
  582. }
  583. end
  584. before { ticket.group.users << user }
  585. let(:perform) do
  586. {
  587. notification_key => {
  588. body: 'Old programmers never die. They just branch to a new address.',
  589. recipient: 'ticket_agents',
  590. subject: 'Old programmers never die. They just branch to a new address.'
  591. }
  592. }.deep_merge(additional_options).deep_stringify_keys
  593. end
  594. let(:notification_key) { "notification.#{notification_type}" }
  595. let!(:ticket_article) { create(:ticket_article, ticket: ticket) }
  596. shared_examples 'verify log visibility status' do
  597. shared_examples 'notification trigger' do
  598. it 'adds Ticket::Article' do
  599. expect { ticket.perform_changes(performable, 'trigger', ticket, user) }
  600. .to change { ticket.articles.count }.by(1)
  601. end
  602. it 'new Ticket::Article visibility reflects setting' do
  603. ticket.perform_changes(performable, 'trigger', ticket, User.first)
  604. new_article = ticket.articles.reload.last
  605. expect(new_article.internal).to be target_internal_value
  606. end
  607. end
  608. context 'when set to private' do
  609. let(:additional_options) do
  610. {
  611. notification_key => {
  612. internal: true
  613. }
  614. }
  615. end
  616. let(:target_internal_value) { true }
  617. it_behaves_like 'notification trigger'
  618. end
  619. context 'when set to internal' do
  620. let(:additional_options) do
  621. {
  622. notification_key => {
  623. internal: false
  624. }
  625. }
  626. end
  627. let(:target_internal_value) { false }
  628. it_behaves_like 'notification trigger'
  629. end
  630. context 'when no selection was made' do # ensure previously created triggers default to public
  631. let(:additional_options) do
  632. {}
  633. end
  634. let(:target_internal_value) { false }
  635. it_behaves_like 'notification trigger'
  636. end
  637. end
  638. context 'dispatching email' do
  639. let(:notification_type) { :email }
  640. include_examples 'verify log visibility status'
  641. end
  642. shared_examples 'add a new article' do
  643. it 'adds a new article' do
  644. expect { ticket.perform_changes(performable, 'trigger', item, user) }
  645. .to change { ticket.articles.count }.by(1)
  646. end
  647. end
  648. shared_examples 'add attachment to new article' do
  649. include_examples 'add a new article'
  650. it 'adds attachment to the new article' do
  651. ticket.perform_changes(performable, 'trigger', item, user)
  652. article = ticket.articles.last
  653. expect(article.type.name).to eq('email')
  654. expect(article.sender.name).to eq('System')
  655. expect(article.attachments.count).to eq(1)
  656. expect(article.attachments[0].filename).to eq('some_file.pdf')
  657. expect(article.attachments[0].preferences['Content-ID']).to eq('image/pdf@01CAB192.K8H512Y9')
  658. end
  659. end
  660. shared_examples 'does not add attachment to new article' do
  661. include_examples 'add a new article'
  662. it 'does not add attachment to the new article' do
  663. ticket.perform_changes(performable, 'trigger', item, user)
  664. article = ticket.articles.last
  665. expect(article.type.name).to eq('email')
  666. expect(article.sender.name).to eq('System')
  667. expect(article.attachments.count).to eq(0)
  668. end
  669. end
  670. context 'dispatching email with include attachment present' do
  671. let(:notification_type) { :email }
  672. let(:additional_options) do
  673. {
  674. notification_key => {
  675. include_attachments: 'true'
  676. }
  677. }
  678. end
  679. context 'when ticket has an attachment' do
  680. before do
  681. UserInfo.current_user_id = 1
  682. create(:store,
  683. object: 'Ticket::Article',
  684. o_id: ticket_article.id,
  685. data: 'dGVzdCAxMjM=',
  686. filename: 'some_file.pdf',
  687. preferences: {
  688. 'Content-Type': 'image/pdf',
  689. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  690. })
  691. end
  692. include_examples 'add attachment to new article'
  693. end
  694. context 'when ticket does not have an attachment' do
  695. include_examples 'does not add attachment to new article'
  696. end
  697. end
  698. context 'dispatching email with include attachment not present' do
  699. let(:notification_type) { :email }
  700. let(:additional_options) do
  701. {
  702. notification_key => {
  703. include_attachments: 'false'
  704. }
  705. }
  706. end
  707. context 'when ticket has an attachment' do
  708. before do
  709. UserInfo.current_user_id = 1
  710. create(:store,
  711. object: 'Ticket::Article',
  712. o_id: ticket_article.id,
  713. data: 'dGVzdCAxMjM=',
  714. filename: 'some_file.pdf',
  715. preferences: {
  716. 'Content-Type': 'image/pdf',
  717. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  718. })
  719. end
  720. include_examples 'does not add attachment to new article'
  721. end
  722. context 'when ticket does not have an attachment' do
  723. include_examples 'does not add attachment to new article'
  724. end
  725. end
  726. context 'dispatching SMS' do
  727. let(:notification_type) { :sms }
  728. before { create(:channel, area: 'Sms::Notification') }
  729. include_examples 'verify log visibility status'
  730. end
  731. end
  732. context 'with a "notification.webhook" trigger', performs_jobs: true do
  733. let(:webhook) { create(:webhook, endpoint: 'http://api.example.com/webhook', signature_token: '53CR3t') }
  734. let(:trigger) do
  735. create(:trigger,
  736. perform: {
  737. 'notification.webhook' => { 'webhook_id' => webhook.id }
  738. })
  739. end
  740. it 'schedules the webhooks notification job' do
  741. expect { ticket.perform_changes(trigger, 'trigger', {}, 1) }.to have_enqueued_job(TriggerWebhookJob).with(
  742. trigger,
  743. ticket,
  744. nil,
  745. changes: {},
  746. user_id: nil,
  747. execution_type: 'trigger',
  748. event_type: nil,
  749. )
  750. end
  751. end
  752. context 'Allow placeholders in trigger perform actions for ticket/custom attributes #4216' do
  753. let(:customer) { create(:customer, mobile: '+491907655431') }
  754. let(:ticket) { create(:ticket, customer: customer) }
  755. let(:perform) do
  756. {
  757. 'ticket.title' => {
  758. 'value' => ticket.customer.mobile.to_s,
  759. }
  760. }
  761. end
  762. it 'does replace custom fields in trigger' do
  763. ticket.perform_changes(performable, 'trigger', ticket, User.first)
  764. expect(ticket.reload.title).to eq(customer.mobile)
  765. end
  766. end
  767. end
  768. describe '#trigger_based_notification?' do
  769. let(:ticket) { create(:ticket) }
  770. context 'with a normal user' do
  771. let(:customer) { create(:customer) }
  772. it 'send trigger base notification' do
  773. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  774. end
  775. end
  776. context 'with a permanent failed user' do
  777. let(:failed_date) { 1.second.ago }
  778. let(:customer) do
  779. user = create(:customer)
  780. user.preferences.merge!(mail_delivery_failed: true, mail_delivery_failed_data: failed_date)
  781. user.save!
  782. user
  783. end
  784. it 'send no trigger base notification' do
  785. expect(ticket.send(:trigger_based_notification?, customer)).to be(false)
  786. expect(customer.reload.preferences[:mail_delivery_failed]).to be(true)
  787. expect(customer.preferences[:mail_delivery_failed_data]).to eq(failed_date)
  788. end
  789. context 'with failed date 61 days ago' do
  790. let(:failed_date) { 61.days.ago }
  791. it 'send trigger base notification' do
  792. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  793. expect(customer.reload.preferences[:mail_delivery_failed]).to be(false)
  794. expect(customer.preferences[:mail_delivery_failed_data]).to be_nil
  795. end
  796. end
  797. context 'with failed date 70 days ago' do
  798. let(:failed_date) { 70.days.ago }
  799. it 'send trigger base notification' do
  800. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  801. expect(customer.reload.preferences[:mail_delivery_failed]).to be(false)
  802. expect(customer.preferences[:mail_delivery_failed_data]).to be_nil
  803. end
  804. end
  805. end
  806. end
  807. describe '#subject_build' do
  808. context 'with default "ticket_hook_position" setting ("right")' do
  809. it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
  810. expect(ticket.subject_build('foo'))
  811. .to eq("foo [Ticket##{ticket.number}]")
  812. end
  813. context 'and a non-default value for the "ticket_hook" setting' do
  814. before { Setting.set('ticket_hook', 'bar baz') }
  815. it 'replaces "Ticket#" with the new ticket hook' do
  816. expect(ticket.subject_build('foo'))
  817. .to eq("foo [bar baz#{ticket.number}]")
  818. end
  819. end
  820. context 'and a non-default value for the "ticket_hook_divider" setting' do
  821. before { Setting.set('ticket_hook_divider', ': ') }
  822. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  823. expect(ticket.subject_build('foo'))
  824. .to eq("foo [Ticket#: #{ticket.number}]")
  825. end
  826. end
  827. context 'when the given string already contains a ticket reference, but in the wrong place' do
  828. it 'moves the ticket reference to the end' do
  829. expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
  830. .to eq("foo [Ticket##{ticket.number}]")
  831. end
  832. end
  833. context 'when the given string already contains an alternately formatted ticket reference' do
  834. it 'reformats the ticket reference' do
  835. expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
  836. .to eq("foo [Ticket##{ticket.number}]")
  837. end
  838. end
  839. end
  840. context 'with alternate "ticket_hook_position" setting ("left")' do
  841. before { Setting.set('ticket_hook_position', 'left') }
  842. it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
  843. expect(ticket.subject_build('foo'))
  844. .to eq("[Ticket##{ticket.number}] foo")
  845. end
  846. context 'and a non-default value for the "ticket_hook" setting' do
  847. before { Setting.set('ticket_hook', 'bar baz') }
  848. it 'replaces "Ticket#" with the new ticket hook' do
  849. expect(ticket.subject_build('foo'))
  850. .to eq("[bar baz#{ticket.number}] foo")
  851. end
  852. end
  853. context 'and a non-default value for the "ticket_hook_divider" setting' do
  854. before { Setting.set('ticket_hook_divider', ': ') }
  855. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  856. expect(ticket.subject_build('foo'))
  857. .to eq("[Ticket#: #{ticket.number}] foo")
  858. end
  859. end
  860. context 'when the given string already contains a ticket reference, but in the wrong place' do
  861. it 'moves the ticket reference to the start' do
  862. expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
  863. .to eq("[Ticket##{ticket.number}] foo")
  864. end
  865. end
  866. context 'when the given string already contains an alternately formatted ticket reference' do
  867. it 'reformats the ticket reference' do
  868. expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
  869. .to eq("[Ticket##{ticket.number}] foo")
  870. end
  871. end
  872. end
  873. end
  874. describe '#last_original_update_at' do
  875. let(:result) { ticket.last_original_update_at }
  876. it 'returns initial customer enquiry time when customer contacted repeatedly' do
  877. ticket
  878. target = create(:ticket_article, :inbound_email, ticket: ticket)
  879. travel 10.minutes
  880. create(:ticket_article, :inbound_email, ticket: ticket)
  881. expect(result).to eq target.created_at
  882. end
  883. it 'returns agent contact time when customer did not respond to agent reach out' do
  884. ticket
  885. create(:ticket_article, :outbound_email, ticket: ticket)
  886. expect(result).to eq ticket.last_contact_agent_at
  887. end
  888. it 'returns nil if no customer response' do
  889. ticket
  890. expect(result).to be_nil
  891. end
  892. context 'with customer enquiry and agent response' do
  893. before do
  894. ticket
  895. create(:ticket_article, :inbound_email, ticket: ticket)
  896. travel 10.minutes
  897. create(:ticket_article, :outbound_email, ticket: ticket)
  898. travel 10.minutes
  899. end
  900. it 'returns last customer enquiry time when agent did not respond yet' do
  901. target = create(:ticket_article, :inbound_email, ticket: ticket)
  902. expect(result).to eq target.created_at
  903. end
  904. it 'returns agent response time when agent responded to customer enquiry' do
  905. expect(result).to eq ticket.last_contact_agent_at
  906. end
  907. end
  908. end
  909. describe '#param_cleanup' do
  910. it 'does only remove parameters which are invalid and not the complete params hash if one element is invalid (#3743)' do
  911. expect(described_class.param_cleanup({ state_id: 3, customer_id: 'guess:1234' }, true, false, false)).to eq({ 'state_id' => 3 })
  912. end
  913. end
  914. end
  915. describe 'Attributes:' do
  916. describe '#owner' do
  917. let(:original_owner) { create(:agent, groups: [ticket.group]) }
  918. before { ticket.update(owner: original_owner) }
  919. context 'when assigned directly' do
  920. context 'to an active agent belonging to ticket.group' do
  921. let(:agent) { create(:agent, groups: [ticket.group]) }
  922. it 'can be set' do
  923. expect { ticket.update(owner: agent) }
  924. .to change { ticket.reload.owner }.to(agent)
  925. end
  926. end
  927. context 'to an agent not belonging to ticket.group' do
  928. let(:agent) { create(:agent, groups: [other_group]) }
  929. let(:other_group) { create(:group) }
  930. it 'resets to default user (id: 1) instead' do
  931. expect { ticket.update(owner: agent) }
  932. .to change { ticket.reload.owner }.to(User.first)
  933. end
  934. end
  935. context 'to an inactive agent' do
  936. let(:agent) { create(:agent, groups: [ticket.group], active: false) }
  937. it 'resets to default user (id: 1) instead' do
  938. expect { ticket.update(owner: agent) }
  939. .to change { ticket.reload.owner }.to(User.first)
  940. end
  941. end
  942. context 'to a non-agent' do
  943. let(:agent) { create(:customer, groups: [ticket.group]) }
  944. it 'resets to default user (id: 1) instead' do
  945. expect { ticket.update(owner: agent) }
  946. .to change { ticket.reload.owner }.to(User.first)
  947. end
  948. end
  949. end
  950. context 'when the ticket is updated for any other reason' do
  951. context 'if original owner is still an active agent belonging to ticket.group' do
  952. it 'does not change' do
  953. expect { create(:ticket_article, ticket: ticket) }
  954. .not_to change { ticket.reload.owner }
  955. end
  956. end
  957. context 'if original owner has left ticket.group' do
  958. before { original_owner.groups = [] }
  959. it 'resets to default user (id: 1)' do
  960. expect { create(:ticket_article, ticket: ticket) }
  961. .to change { ticket.reload.owner }.to(User.first)
  962. end
  963. end
  964. context 'if original owner has become inactive' do
  965. before { original_owner.update(active: false) }
  966. it 'resets to default user (id: 1)' do
  967. expect { create(:ticket_article, ticket: ticket) }
  968. .to change { ticket.reload.owner }.to(User.first)
  969. end
  970. end
  971. context 'if original owner has lost agent status' do
  972. before { original_owner.roles = [create(:role)] }
  973. it 'resets to default user (id: 1)' do
  974. Rails.cache.clear
  975. expect { create(:ticket_article, ticket: ticket) }
  976. .to change { ticket.reload.owner }.to(User.first)
  977. end
  978. end
  979. context 'when the Ticket is closed' do
  980. before do
  981. ticket.update!(state: Ticket::State.lookup(name: 'closed'))
  982. end
  983. context 'if original owner is still an active agent belonging to ticket.group' do
  984. it 'does not change' do
  985. expect { create(:ticket_article, ticket: ticket) }
  986. .not_to change { ticket.reload.owner }
  987. end
  988. end
  989. context 'if original owner has left ticket.group' do
  990. before { original_owner.groups = [] }
  991. it 'does not change' do
  992. expect { create(:ticket_article, ticket: ticket) }
  993. .not_to change { ticket.reload.owner }
  994. end
  995. end
  996. context 'if original owner has become inactive' do
  997. before { original_owner.update(active: false) }
  998. it 'does not change' do
  999. expect { create(:ticket_article, ticket: ticket) }
  1000. .not_to change { ticket.reload.owner }
  1001. end
  1002. end
  1003. context 'if original owner has lost agent status' do
  1004. before { original_owner.roles = [create(:role)] }
  1005. it 'does not change' do
  1006. expect { create(:ticket_article, ticket: ticket) }
  1007. .not_to change { ticket.reload.owner }
  1008. end
  1009. end
  1010. end
  1011. end
  1012. end
  1013. describe '#state' do
  1014. context 'when originally "new" (default)' do
  1015. context 'and a customer article is added' do
  1016. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
  1017. it 'stays "new"' do
  1018. expect { article }
  1019. .not_to change { ticket.state.name }.from('new')
  1020. end
  1021. end
  1022. context 'and a non-customer article is added' do
  1023. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1024. it 'switches to "open"' do
  1025. expect { article }
  1026. .to change { ticket.reload.state.name }.from('new').to('open')
  1027. end
  1028. end
  1029. end
  1030. context 'when originally "closed"' do
  1031. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  1032. context 'when a non-customer article is added' do
  1033. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1034. it 'stays "closed"' do
  1035. expect { article }.not_to change { ticket.reload.state.name }
  1036. end
  1037. end
  1038. end
  1039. end
  1040. describe '#pending_time' do
  1041. subject(:ticket) { create(:ticket, pending_time: 2.days.from_now) }
  1042. context 'when #state is updated to any non-"pending" value' do
  1043. it 'is reset to nil' do
  1044. expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
  1045. .to change(ticket, :pending_time).to(nil)
  1046. end
  1047. end
  1048. # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
  1049. context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
  1050. it 'does not prematurely raise within the callback (#reset_pending_time)' do
  1051. expect { ticket.update!(state: nil) }
  1052. .to raise_error(ActiveRecord::StatementInvalid)
  1053. end
  1054. end
  1055. end
  1056. describe '#escalation_at' do
  1057. before { freeze_time } # freeze time
  1058. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1059. let(:calendar) { create(:calendar, :'24/7') }
  1060. context 'with no SLAs in the system' do
  1061. it 'defaults to nil' do
  1062. expect(ticket.escalation_at).to be_nil
  1063. end
  1064. end
  1065. context 'with an SLA in the system' do
  1066. before { sla } # create sla
  1067. it 'is set based on SLA’s #first_response_time' do
  1068. expect(ticket.reload.escalation_at.to_i)
  1069. .to eq(1.hour.from_now.to_i)
  1070. end
  1071. context 'after first agent’s response' do
  1072. before { ticket } # create ticket
  1073. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1074. it 'is updated based on the SLA’s #close_escalation_at' do
  1075. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  1076. expect { article }
  1077. .to change { ticket.reload.escalation_at }
  1078. .to(ticket.reload.close_escalation_at)
  1079. end
  1080. context 'when new #update_time is later than original #solution_time' do
  1081. it 'is updated based on the original #solution_time' do
  1082. travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  1083. expect { article }
  1084. .to change { ticket.reload.escalation_at }
  1085. .to(4.hours.after(ticket.created_at))
  1086. end
  1087. end
  1088. end
  1089. end
  1090. context 'when updated after an SLA has been added to the system' do
  1091. before do
  1092. ticket # create ticket
  1093. sla # create sla
  1094. end
  1095. it 'is updated based on the new SLA’s #first_response_time' do
  1096. expect { ticket.save! }
  1097. .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
  1098. end
  1099. end
  1100. context 'when updated after all SLAs have been removed from the system' do
  1101. before do
  1102. sla # create sla
  1103. ticket # create ticket
  1104. sla.destroy
  1105. end
  1106. it 'is set to nil' do
  1107. expect { ticket.save! }
  1108. .to change { ticket.reload.escalation_at }.to(nil)
  1109. end
  1110. end
  1111. context 'when within last (relative)' do
  1112. let(:first_response_time) { 5 }
  1113. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1114. let(:within_condition) do
  1115. { 'ticket.escalation_at'=>{ 'operator' => 'within last (relative)', 'value' => '30', 'range' => 'minute' } }
  1116. end
  1117. before do
  1118. sla
  1119. travel_to '2020-11-05 11:37:00'
  1120. ticket = create(:ticket)
  1121. create(:ticket_article, :inbound_email, ticket: ticket)
  1122. travel_to '2020-11-05 11:50:00'
  1123. end
  1124. context 'when in range' do
  1125. it 'does find the ticket' do
  1126. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1127. expect(count).to eq(1)
  1128. end
  1129. end
  1130. context 'when out of range' do
  1131. let(:first_response_time) { 500 }
  1132. it 'does not find the ticket' do
  1133. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1134. expect(count).to eq(0)
  1135. end
  1136. end
  1137. end
  1138. context 'when till (relative)' do
  1139. let(:first_response_time) { 5 }
  1140. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1141. let(:condition) do
  1142. { 'ticket.escalation_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }
  1143. end
  1144. before do
  1145. sla
  1146. travel_to '2020-11-05 11:37:00'
  1147. ticket = create(:ticket)
  1148. create(:ticket_article, :inbound_email, ticket: ticket)
  1149. travel_to '2020-11-05 11:50:00'
  1150. end
  1151. context 'when in range' do
  1152. it 'does find the ticket' do
  1153. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1154. expect(count).to eq(1)
  1155. end
  1156. end
  1157. context 'when out of range' do
  1158. let(:first_response_time) { 500 }
  1159. it 'does not find the ticket' do
  1160. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1161. expect(count).to eq(0)
  1162. end
  1163. end
  1164. end
  1165. context 'when from (relative)' do
  1166. let(:first_response_time) { 5 }
  1167. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1168. let(:condition) do
  1169. { 'ticket.escalation_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } }
  1170. end
  1171. before do
  1172. sla
  1173. travel_to '2020-11-05 11:37:00'
  1174. ticket = create(:ticket)
  1175. create(:ticket_article, :inbound_email, ticket: ticket)
  1176. end
  1177. context 'when in range' do
  1178. it 'does find the ticket' do
  1179. travel_to '2020-11-05 11:50:00'
  1180. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1181. expect(count).to eq(1)
  1182. end
  1183. end
  1184. context 'when out of range' do
  1185. let(:first_response_time) { 5 }
  1186. it 'does not find the ticket' do
  1187. travel_to '2020-11-05 13:50:00'
  1188. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1189. expect(count).to eq(0)
  1190. end
  1191. end
  1192. end
  1193. context 'when within next (relative)' do
  1194. let(:first_response_time) { 5 }
  1195. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1196. let(:within_condition) do
  1197. { 'ticket.escalation_at'=>{ 'operator' => 'within next (relative)', 'value' => '30', 'range' => 'minute' } }
  1198. end
  1199. before do
  1200. sla
  1201. travel_to '2020-11-05 11:50:00'
  1202. ticket = create(:ticket)
  1203. create(:ticket_article, :inbound_email, ticket: ticket)
  1204. travel_to '2020-11-05 11:37:00'
  1205. end
  1206. context 'when in range' do
  1207. it 'does find the ticket' do
  1208. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1209. expect(count).to eq(1)
  1210. end
  1211. end
  1212. context 'when out of range' do
  1213. let(:first_response_time) { 500 }
  1214. it 'does not find the ticket' do
  1215. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1216. expect(count).to eq(0)
  1217. end
  1218. end
  1219. end
  1220. end
  1221. describe '#first_response_escalation_at' do
  1222. before { freeze_time } # freeze time
  1223. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1224. let(:calendar) { create(:calendar, :'24/7') }
  1225. context 'with no SLAs in the system' do
  1226. it 'defaults to nil' do
  1227. expect(ticket.first_response_escalation_at).to be_nil
  1228. end
  1229. end
  1230. context 'with an SLA in the system' do
  1231. before { sla } # create sla
  1232. it 'is set based on SLA’s #first_response_time' do
  1233. expect(ticket.reload.first_response_escalation_at.to_i)
  1234. .to eq(1.hour.from_now.to_i)
  1235. end
  1236. context 'after first agent’s response' do
  1237. before { ticket } # create ticket
  1238. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1239. it 'is cleared' do
  1240. expect { article }.to change { ticket.reload.first_response_escalation_at }.to(nil)
  1241. end
  1242. end
  1243. end
  1244. end
  1245. describe '#update_escalation_at' do
  1246. before { freeze_time } # freeze time
  1247. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1248. let(:calendar) { create(:calendar, :'24/7') }
  1249. context 'with no SLAs in the system' do
  1250. it 'defaults to nil' do
  1251. expect(ticket.update_escalation_at).to be_nil
  1252. end
  1253. end
  1254. context 'with an SLA in the system' do
  1255. before { sla } # create sla
  1256. it 'is set based on SLA’s #update_time' do
  1257. travel 1.minute
  1258. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  1259. expect(ticket.reload.update_escalation_at.to_i)
  1260. .to eq(3.hours.from_now.to_i)
  1261. end
  1262. context 'after first agent’s response' do
  1263. before { ticket } # create ticket
  1264. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1265. it 'is updated based on the SLA’s #update_time' do
  1266. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  1267. travel(1.minute)
  1268. expect { article }
  1269. .to change { ticket.reload.update_escalation_at }
  1270. .to(nil)
  1271. end
  1272. end
  1273. end
  1274. end
  1275. describe '#close_escalation_at' do
  1276. before { freeze_time } # freeze time
  1277. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1278. let(:calendar) { create(:calendar, :'24/7') }
  1279. context 'with no SLAs in the system' do
  1280. it 'defaults to nil' do
  1281. expect(ticket.close_escalation_at).to be_nil
  1282. end
  1283. end
  1284. context 'with an SLA in the system' do
  1285. before { sla } # create sla
  1286. it 'is set based on SLA’s #solution_time' do
  1287. expect(ticket.reload.close_escalation_at.to_i)
  1288. .to eq(4.hours.from_now.to_i)
  1289. end
  1290. context 'after first agent’s response' do
  1291. before { ticket } # create ticket
  1292. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1293. it 'does not change' do
  1294. expect { article }.not_to change(ticket, :close_escalation_at)
  1295. end
  1296. end
  1297. end
  1298. end
  1299. end
  1300. describe '.search' do
  1301. shared_examples 'search permissions' do
  1302. let(:group) { create(:group) }
  1303. before do
  1304. ticket
  1305. end
  1306. shared_examples 'permitted' do
  1307. it 'finds Ticket' do
  1308. expect(described_class.search(query: ticket.number, current_user: current_user).count).to eq(1)
  1309. end
  1310. end
  1311. shared_examples 'no permission' do
  1312. it "doesn't find Ticket" do
  1313. expect(described_class.search(query: ticket.number, current_user: current_user)).to be_blank
  1314. end
  1315. end
  1316. context 'Agent with Group access' do
  1317. let(:ticket) do
  1318. ticket = create(:ticket, group: group)
  1319. create(:ticket_article, ticket: ticket)
  1320. ticket
  1321. end
  1322. let(:current_user) { create(:agent, groups: [group]) }
  1323. it_behaves_like 'permitted'
  1324. end
  1325. context 'when Agent is Customer of Ticket' do
  1326. let(:ticket) do
  1327. ticket = create(:ticket, customer: current_user)
  1328. create(:ticket_article, ticket: ticket)
  1329. ticket
  1330. end
  1331. let(:current_user) { create(:agent_and_customer) }
  1332. it_behaves_like 'permitted'
  1333. end
  1334. context 'for Organization access' do
  1335. let(:ticket) do
  1336. ticket = create(:ticket, customer: customer)
  1337. create(:ticket_article, ticket: ticket)
  1338. ticket
  1339. end
  1340. let(:customer) { create(:customer, organization: organization) }
  1341. context 'when Organization is shared' do
  1342. let(:organization) { create(:organization, shared: true) }
  1343. context 'for unrelated Agent' do
  1344. let(:current_user) { create(:agent) }
  1345. it_behaves_like 'no permission'
  1346. end
  1347. context 'for Agent in same Organization' do
  1348. let(:current_user) { create(:agent_and_customer, organization: organization) }
  1349. it_behaves_like 'permitted'
  1350. end
  1351. context 'for Customer of Ticket' do
  1352. let(:current_user) { customer }
  1353. it_behaves_like 'permitted'
  1354. end
  1355. end
  1356. context 'when Organization is not shared' do
  1357. let(:organization) { create(:organization, shared: false) }
  1358. context 'for unrelated Agent' do
  1359. let(:current_user) { create(:agent) }
  1360. it_behaves_like 'no permission'
  1361. end
  1362. context 'for Agent in same Organization' do
  1363. let(:current_user) { create(:agent_and_customer, organization: organization) }
  1364. it_behaves_like 'no permission'
  1365. end
  1366. context 'for Customer of Ticket' do
  1367. let(:current_user) { customer }
  1368. it_behaves_like 'permitted'
  1369. end
  1370. end
  1371. end
  1372. end
  1373. context 'with searchindex', searchindex: true do
  1374. include_examples 'search permissions' do
  1375. before do
  1376. searchindex_model_reload([described_class])
  1377. end
  1378. end
  1379. end
  1380. context 'without searchindex' do
  1381. before do
  1382. Setting.set('es_url', nil)
  1383. end
  1384. include_examples 'search permissions'
  1385. end
  1386. end
  1387. describe 'Callbacks & Observers -' do
  1388. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  1389. it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
  1390. expect { create(:ticket, title: "some title \u0000 123") }
  1391. .not_to raise_error
  1392. end
  1393. end
  1394. describe 'XSS protection:' do
  1395. subject(:ticket) { create(:ticket, title: title) }
  1396. let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
  1397. it 'does not sanitize title' do
  1398. expect(ticket.title).to eq(title)
  1399. end
  1400. end
  1401. describe 'Cti::CallerId syncing:', performs_jobs: true do
  1402. subject(:ticket) { build(:ticket) }
  1403. before { allow(Cti::CallerId).to receive(:build) }
  1404. it 'adds numbers in article bodies (via Cti::CallerId.build)' do
  1405. expect(Cti::CallerId).to receive(:build).with(ticket)
  1406. ticket.save
  1407. perform_enqueued_jobs commit_transaction: true
  1408. end
  1409. end
  1410. describe 'Touching associations on update:' do
  1411. subject(:ticket) { create(:ticket, customer: customer) }
  1412. let(:customer) { create(:customer, organization: organization) }
  1413. let(:organization) { create(:organization) }
  1414. let(:other_customer) { create(:customer, organization: other_organization) }
  1415. let(:other_organization) { create(:organization) }
  1416. context 'on creation' do
  1417. it 'touches its customer and his organization' do
  1418. expect { ticket }
  1419. .to change { customer.reload.updated_at }
  1420. .and change { organization.reload.updated_at }
  1421. end
  1422. end
  1423. context 'on destruction' do
  1424. before { ticket }
  1425. it 'touches its customer and his organization' do
  1426. expect { ticket.destroy }
  1427. .to change { customer.reload.updated_at }
  1428. .and change { organization.reload.updated_at }
  1429. end
  1430. end
  1431. context 'when customer association is changed' do
  1432. it 'touches both old and new customer, and their organizations' do
  1433. expect { ticket.update(customer: other_customer) }
  1434. .to change { customer.reload.updated_at }
  1435. .and change { organization.reload.updated_at }
  1436. .and change { other_customer.reload.updated_at }
  1437. .and change { other_organization.reload.updated_at }
  1438. end
  1439. end
  1440. end
  1441. describe 'Association & attachment management:' do
  1442. it 'deletes all related ActivityStreams on destroy' do
  1443. create_list(:activity_stream, 3, o: ticket)
  1444. expect { ticket.destroy }
  1445. .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1446. .to(false)
  1447. end
  1448. it 'deletes all related Links on destroy' do
  1449. create(:link, from: ticket, to: create(:ticket))
  1450. create(:link, from: create(:ticket), to: ticket)
  1451. create(:link, from: ticket, to: create(:ticket))
  1452. expect { ticket.destroy }
  1453. .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
  1454. .to(false)
  1455. end
  1456. it 'deletes all related Articles on destroy' do
  1457. create_list(:ticket_article, 3, ticket: ticket)
  1458. expect { ticket.destroy }
  1459. .to change { Ticket::Article.exists?(ticket: ticket) }
  1460. .to(false)
  1461. end
  1462. it 'deletes all related OnlineNotifications on destroy' do
  1463. create_list(:online_notification, 3, o: ticket)
  1464. expect { ticket.destroy }
  1465. .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
  1466. .to(false)
  1467. end
  1468. it 'deletes all related Tags on destroy' do
  1469. create_list(:tag, 3, o: ticket)
  1470. expect { ticket.destroy }
  1471. .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1472. .to(false)
  1473. end
  1474. it 'deletes all related Histories on destroy' do
  1475. create_list(:history, 3, o: ticket)
  1476. expect { ticket.destroy }
  1477. .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1478. .to(false)
  1479. end
  1480. it 'deletes all related RecentViews on destroy' do
  1481. create_list(:recent_view, 3, o: ticket)
  1482. expect { ticket.destroy }
  1483. .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1484. .to(false)
  1485. end
  1486. it 'destroys all related dependencies' do
  1487. refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
  1488. 'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
  1489. 'Ticket::SharedDraftZoom' => { 'ticket_id'=>0 },
  1490. 'Ticket::Flag' => { 'ticket_id'=>1 } }
  1491. ticket = create(:ticket)
  1492. article = create(:ticket_article, ticket: ticket)
  1493. accounting = create(:ticket_time_accounting, ticket: ticket)
  1494. flag = create(:ticket_flag, ticket: ticket)
  1495. refs_ticket = Models.references('Ticket', ticket.id, true)
  1496. expect(refs_ticket).to eq(refs_known)
  1497. ticket.destroy
  1498. expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1499. expect { article.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1500. expect { accounting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1501. expect { flag.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1502. end
  1503. context 'when ticket is generated from email (with attachments)' do
  1504. subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
  1505. let(:raw_email) { Rails.root.join('test/data/mail/mail001.box').read }
  1506. it 'adds attachments to the Store{::File,::Provider::DB} tables' do
  1507. expect { ticket }
  1508. .to change(Store, :count).by(2)
  1509. .and change(Store::File, :count).by(2)
  1510. .and change(Store::Provider::DB, :count).by(2)
  1511. end
  1512. context 'and subsequently destroyed' do
  1513. it 'deletes all related attachments' do
  1514. ticket # create ticket
  1515. expect { ticket.destroy }
  1516. .to change(Store, :count).by(-2)
  1517. .and change(Store::File, :count).by(-2)
  1518. .and change(Store::Provider::DB, :count).by(-2)
  1519. end
  1520. end
  1521. context 'and a duplicate ticket is generated from the same email' do
  1522. before { ticket } # create ticket
  1523. let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
  1524. it 'adds duplicate attachments to the Store table only' do
  1525. expect { duplicate }
  1526. .to change(Store, :count).by(2)
  1527. .and not_change(Store::File, :count)
  1528. .and not_change(Store::Provider::DB, :count)
  1529. end
  1530. context 'when only the duplicate ticket is destroyed' do
  1531. it 'deletes only the duplicate attachments' do
  1532. duplicate # create ticket
  1533. expect { duplicate.destroy }
  1534. .to change(Store, :count).by(-2)
  1535. .and not_change(Store::File, :count)
  1536. .and not_change(Store::Provider::DB, :count)
  1537. end
  1538. it 'deletes all related attachments' do
  1539. duplicate.destroy
  1540. expect { ticket.destroy }
  1541. .to change(Store, :count).by(-2)
  1542. .and change(Store::File, :count).by(-2)
  1543. .and change(Store::Provider::DB, :count).by(-2)
  1544. end
  1545. end
  1546. end
  1547. end
  1548. end
  1549. describe 'Ticket lifecycle order-of-operations:', performs_jobs: true do
  1550. subject!(:ticket) { create(:ticket) }
  1551. let!(:agent) { create(:agent, groups: [group]) }
  1552. let(:group) { create(:group) }
  1553. before do
  1554. create(
  1555. :trigger,
  1556. condition: { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } },
  1557. perform: { 'ticket.group_id' => { 'value' => group.id } }
  1558. )
  1559. end
  1560. it 'fires triggers before new ticket notifications are sent' do
  1561. expect { TransactionDispatcher.commit }
  1562. .to change { ticket.reload.group }.to(group)
  1563. expect { perform_enqueued_jobs }
  1564. .to change { NotificationFactory::Mailer.already_sent?(ticket, agent, 'email') }.to(1)
  1565. end
  1566. end
  1567. describe 'Ticket has changed attributes:' do
  1568. subject!(:ticket) { create(:ticket) }
  1569. let(:group) { create(:group) }
  1570. let(:condition_field) { nil }
  1571. shared_examples 'updated ticket group with trigger condition' do
  1572. it 'updated ticket group with has changed trigger condition' do
  1573. expect { TransactionDispatcher.commit }.to change { ticket.reload.group }.to(group)
  1574. end
  1575. end
  1576. before do
  1577. create(
  1578. :trigger,
  1579. condition: { "ticket.#{condition_field}" => { 'operator' => 'has changed', 'value' => 'create' } },
  1580. perform: { 'ticket.group_id' => { 'value' => group.id } }
  1581. )
  1582. ticket.update!(condition_field => Time.zone.now)
  1583. end
  1584. context "when changing 'first_response_at' attribute" do
  1585. let(:condition_field) { 'first_response_at' }
  1586. include_examples 'updated ticket group with trigger condition'
  1587. end
  1588. context "when changing 'close_at' attribute" do
  1589. let(:condition_field) { 'close_at' }
  1590. include_examples 'updated ticket group with trigger condition'
  1591. end
  1592. context "when changing 'last_contact_agent_at' attribute" do
  1593. let(:condition_field) { 'last_contact_agent_at' }
  1594. include_examples 'updated ticket group with trigger condition'
  1595. end
  1596. context "when changing 'last_contact_customer_at' attribute" do
  1597. let(:condition_field) { 'last_contact_customer_at' }
  1598. include_examples 'updated ticket group with trigger condition'
  1599. end
  1600. context "when changing 'last_contact_at' attribute" do
  1601. let(:condition_field) { 'last_contact_at' }
  1602. include_examples 'updated ticket group with trigger condition'
  1603. end
  1604. end
  1605. end
  1606. describe 'Mentions:', sends_notification_emails: true do
  1607. context 'when notifications', performs_jobs: true do
  1608. let(:prefs_matrix_no_mentions) do
  1609. { 'notification_config' =>
  1610. { 'matrix' =>
  1611. { 'create' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1612. 'update' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1613. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1614. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1615. end
  1616. let(:prefs_matrix_only_mentions) do
  1617. { 'notification_config' =>
  1618. { 'matrix' =>
  1619. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1620. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1621. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1622. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1623. end
  1624. let(:prefs_matrix_only_mentions_groups) do
  1625. { 'notification_config' =>
  1626. { 'matrix' =>
  1627. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1628. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1629. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1630. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } },
  1631. 'group_ids' => [create(:group).id, create(:group).id, create(:group).id] } }
  1632. end
  1633. let(:mention_group) { create(:group) }
  1634. let(:no_access_group) { create(:group) }
  1635. let(:user_only_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions) }
  1636. let(:user_read_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions_groups) }
  1637. let(:user_no_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_no_mentions) }
  1638. let(:ticket) { create(:ticket, group: mention_group, owner: user_no_mentions) }
  1639. it 'does inform mention user about the ticket update' do
  1640. create(:mention, mentionable: ticket, user: user_only_mentions)
  1641. create(:mention, mentionable: ticket, user: user_read_mentions)
  1642. create(:mention, mentionable: ticket, user: user_no_mentions)
  1643. perform_enqueued_jobs commit_transaction: true
  1644. check_notification do
  1645. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1646. perform_enqueued_jobs commit_transaction: true
  1647. sent(
  1648. template: 'ticket_update',
  1649. user: user_no_mentions,
  1650. )
  1651. sent(
  1652. template: 'ticket_update',
  1653. user: user_read_mentions,
  1654. )
  1655. sent(
  1656. template: 'ticket_update',
  1657. user: user_only_mentions,
  1658. )
  1659. end
  1660. end
  1661. it 'does not inform mention user about the ticket update' do
  1662. ticket
  1663. perform_enqueued_jobs commit_transaction: true
  1664. check_notification do
  1665. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1666. perform_enqueued_jobs commit_transaction: true
  1667. sent(
  1668. template: 'ticket_update',
  1669. user: user_no_mentions,
  1670. )
  1671. not_sent(
  1672. template: 'ticket_update',
  1673. user: user_read_mentions,
  1674. )
  1675. not_sent(
  1676. template: 'ticket_update',
  1677. user: user_only_mentions,
  1678. )
  1679. end
  1680. end
  1681. it 'does inform mention user about ticket creation' do
  1682. check_notification do
  1683. ticket = create(:ticket, owner: user_no_mentions, group: mention_group)
  1684. create(:mention, mentionable: ticket, user: user_read_mentions)
  1685. create(:mention, mentionable: ticket, user: user_only_mentions)
  1686. perform_enqueued_jobs commit_transaction: true
  1687. sent(
  1688. template: 'ticket_create',
  1689. user: user_no_mentions,
  1690. )
  1691. sent(
  1692. template: 'ticket_create',
  1693. user: user_read_mentions,
  1694. )
  1695. sent(
  1696. template: 'ticket_create',
  1697. user: user_only_mentions,
  1698. )
  1699. end
  1700. end
  1701. it 'does not inform mention user about ticket creation' do
  1702. check_notification do
  1703. create(:ticket, owner: user_no_mentions, group: mention_group)
  1704. perform_enqueued_jobs commit_transaction: true
  1705. sent(
  1706. template: 'ticket_create',
  1707. user: user_no_mentions,
  1708. )
  1709. not_sent(
  1710. template: 'ticket_create',
  1711. user: user_read_mentions,
  1712. )
  1713. not_sent(
  1714. template: 'ticket_create',
  1715. user: user_only_mentions,
  1716. )
  1717. end
  1718. end
  1719. it 'does not inform mention user about ticket creation because of no permissions' do
  1720. check_notification do
  1721. ticket = create(:ticket, group: no_access_group)
  1722. build(:mention, mentionable: ticket, user: user_read_mentions).save!(validate: false)
  1723. build(:mention, mentionable: ticket, user: user_only_mentions).save!(validate: false)
  1724. perform_enqueued_jobs commit_transaction: true
  1725. not_sent(
  1726. template: 'ticket_create',
  1727. user: user_read_mentions,
  1728. )
  1729. not_sent(
  1730. template: 'ticket_create',
  1731. user: user_only_mentions,
  1732. )
  1733. end
  1734. end
  1735. end
  1736. context 'selectors' do
  1737. let(:mention_group) { create(:group) }
  1738. let(:ticket_mentions) { create(:ticket, group: mention_group) }
  1739. let(:ticket_normal) { create(:ticket, group: mention_group) }
  1740. let(:user_mentions) { create(:agent, groups: [mention_group]) }
  1741. let(:user_no_mentions) { create(:agent, groups: [mention_group]) }
  1742. before do
  1743. described_class.destroy_all
  1744. ticket_normal
  1745. user_no_mentions
  1746. create(:mention, mentionable: ticket_mentions, user: user_mentions)
  1747. end
  1748. it 'pre condition is not_set' do
  1749. condition = {
  1750. 'ticket.mention_user_ids' => {
  1751. pre_condition: 'not_set',
  1752. operator: 'is',
  1753. },
  1754. }
  1755. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1756. .to contain_exactly(1, [ticket_normal].to_a)
  1757. end
  1758. it 'pre condition is not not_set' do
  1759. condition = {
  1760. 'ticket.mention_user_ids' => {
  1761. pre_condition: 'not_set',
  1762. operator: 'is not',
  1763. },
  1764. }
  1765. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1766. .to contain_exactly(1, [ticket_mentions].to_a)
  1767. end
  1768. it 'pre condition is current_user.id' do
  1769. condition = {
  1770. 'ticket.mention_user_ids' => {
  1771. pre_condition: 'current_user.id',
  1772. operator: 'is',
  1773. },
  1774. }
  1775. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1776. .to contain_exactly(1, [ticket_mentions].to_a)
  1777. end
  1778. it 'pre condition is not current_user.id' do
  1779. condition = {
  1780. 'ticket.mention_user_ids' => {
  1781. pre_condition: 'current_user.id',
  1782. operator: 'is not',
  1783. },
  1784. }
  1785. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1786. .to contain_exactly(0, [])
  1787. end
  1788. it 'pre condition is specific' do
  1789. condition = {
  1790. 'ticket.mention_user_ids' => {
  1791. pre_condition: 'specific',
  1792. operator: 'is',
  1793. value: user_mentions.id
  1794. },
  1795. }
  1796. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1797. .to contain_exactly(1, [ticket_mentions].to_a)
  1798. end
  1799. it 'pre condition is not specific' do
  1800. condition = {
  1801. 'ticket.mention_user_ids' => {
  1802. pre_condition: 'specific',
  1803. operator: 'is not',
  1804. value: user_mentions.id
  1805. },
  1806. }
  1807. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1808. .to contain_exactly(0, [])
  1809. end
  1810. end
  1811. end
  1812. describe '.search_index_attribute_lookup_oversized?' do
  1813. subject!(:ticket) { create(:ticket) }
  1814. context 'when payload is ok' do
  1815. let(:current_payload_size) { 3.megabyte }
  1816. it 'return false' do
  1817. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be false
  1818. end
  1819. end
  1820. context 'when payload is bigger' do
  1821. let(:current_payload_size) { 350.megabyte }
  1822. it 'return true' do
  1823. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be true
  1824. end
  1825. end
  1826. end
  1827. describe '.search_index_attribute_lookup_file_oversized?' do
  1828. subject!(:store) do
  1829. create(:store,
  1830. object: 'SomeObject',
  1831. o_id: 1,
  1832. data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
  1833. filename: 'test.TXT')
  1834. end
  1835. context 'when total payload is ok' do
  1836. let(:current_payload_size) { 200.megabyte }
  1837. it 'return false' do
  1838. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be false
  1839. end
  1840. end
  1841. context 'when total payload is oversized' do
  1842. let(:current_payload_size) { 299.megabyte }
  1843. it 'return true' do
  1844. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be true
  1845. end
  1846. end
  1847. end
  1848. describe '.search_index_attribute_lookup_file_ignored?' do
  1849. context 'when attachment is indexable' do
  1850. subject!(:store_with_indexable_extention) do
  1851. create(:store,
  1852. object: 'SomeObject',
  1853. o_id: 1,
  1854. data: 'some content',
  1855. filename: 'test.TXT')
  1856. end
  1857. it 'return false' do
  1858. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_with_indexable_extention)).to be false
  1859. end
  1860. end
  1861. context 'when attachment is no indexable' do
  1862. subject!(:store_without_indexable_extention) do
  1863. create(:store,
  1864. object: 'SomeObject',
  1865. o_id: 1,
  1866. data: 'some content',
  1867. filename: 'test.BIN')
  1868. end
  1869. it 'return true' do
  1870. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_without_indexable_extention)).to be true
  1871. end
  1872. end
  1873. end
  1874. describe '.search_index_attribute_lookup' do
  1875. subject!(:ticket) { create(:ticket) }
  1876. let(:search_index_attribute_lookup) do
  1877. article1 = create(:ticket_article, ticket: ticket)
  1878. create(:store,
  1879. object: 'Ticket::Article',
  1880. o_id: article1.id,
  1881. data: 'some content',
  1882. filename: 'some_file.bin',
  1883. preferences: {
  1884. 'Content-Type' => 'text/plain',
  1885. })
  1886. create(:store,
  1887. object: 'Ticket::Article',
  1888. o_id: article1.id,
  1889. data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
  1890. filename: 'some_file.pdf',
  1891. preferences: {
  1892. 'Content-Type' => 'image/pdf',
  1893. })
  1894. create(:store,
  1895. object: 'Ticket::Article',
  1896. o_id: article1.id,
  1897. data: 'a' * ((1024**2) * 5.8), # with 5,8 mb
  1898. filename: 'some_file.txt',
  1899. preferences: {
  1900. 'Content-Type' => 'text/plain',
  1901. })
  1902. create(:ticket_article, ticket: ticket, body: 'a' * ((1024**2) * 1.2)) # body with 1,2 mb
  1903. create(:ticket_article, ticket: ticket)
  1904. ticket.search_index_attribute_lookup
  1905. end
  1906. context 'when es_attachment_max_size_in_mb takes all attachments' do
  1907. before { Setting.set('es_attachment_max_size_in_mb', 15) }
  1908. it 'verify count of articles' do
  1909. expect(search_index_attribute_lookup['article'].count).to eq 3
  1910. end
  1911. it 'verify count of attachments' do
  1912. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 2
  1913. end
  1914. it 'verify if pdf exists' do
  1915. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1916. end
  1917. it 'verify if txt exists' do
  1918. expect(search_index_attribute_lookup['article'][0]['attachment'][1]['_name']).to eq 'some_file.txt'
  1919. end
  1920. end
  1921. context 'when es_attachment_max_size_in_mb takes only one attachment' do
  1922. before { Setting.set('es_attachment_max_size_in_mb', 4) }
  1923. it 'verify count of articles' do
  1924. expect(search_index_attribute_lookup['article'].count).to eq 3
  1925. end
  1926. it 'verify count of attachments' do
  1927. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 1
  1928. end
  1929. it 'verify if pdf exists' do
  1930. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1931. end
  1932. end
  1933. context 'when es_attachment_max_size_in_mb takes no attachment' do
  1934. before { Setting.set('es_attachment_max_size_in_mb', 2) }
  1935. it 'verify count of articles' do
  1936. expect(search_index_attribute_lookup['article'].count).to eq 3
  1937. end
  1938. it 'verify count of attachments' do
  1939. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1940. end
  1941. end
  1942. context 'when es_total_max_size_in_mb takes no attachment and no oversized article' do
  1943. before { Setting.set('es_total_max_size_in_mb', 1) }
  1944. it 'verify count of articles' do
  1945. expect(search_index_attribute_lookup['article'].count).to eq 2
  1946. end
  1947. it 'verify count of attachments' do
  1948. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1949. end
  1950. end
  1951. end
  1952. describe '#reopen_after_certain_time?' do
  1953. context 'when groups.follow_up_possible is set to "new_ticket_after_certain_time"' do
  1954. let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: 2) }
  1955. context 'when ticket is open' do
  1956. let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'open')) }
  1957. it 'returns false' do
  1958. expect(ticket.reopen_after_certain_time?).to be false
  1959. end
  1960. end
  1961. context 'when ticket is closed' do
  1962. let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'closed')) }
  1963. context 'when it is within configured time frame' do
  1964. it 'returns true' do
  1965. expect(ticket.reopen_after_certain_time?).to be true
  1966. end
  1967. end
  1968. context 'when it is outside configured time frame' do
  1969. before do
  1970. ticket
  1971. travel 3.days
  1972. end
  1973. it 'returns false' do
  1974. expect(ticket.reopen_after_certain_time?).to be false
  1975. end
  1976. end
  1977. end
  1978. context 'when reopen_time_in_days is not set' do
  1979. let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: -1) }
  1980. it 'returns false' do
  1981. expect(ticket.reopen_after_certain_time?).to be false
  1982. end
  1983. end
  1984. end
  1985. end
  1986. describe 'Automatic assignment assigns tickets in each group, not just the marked ones #4308' do
  1987. let(:ticket) { create(:ticket, group: Group.first, state: Ticket::State.find_by(name: 'closed')) }
  1988. let(:agent) { create(:agent, groups: [Group.first]) }
  1989. context 'when the condition does match' do
  1990. before do
  1991. Setting.set('ticket_auto_assignment', true)
  1992. Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.all.pluck(:id) } } })
  1993. end
  1994. it 'does auto assign' do
  1995. ticket.auto_assign(agent)
  1996. expect(ticket.reload.owner_id).to eq(agent.id)
  1997. end
  1998. end
  1999. context 'when the condition does not match' do
  2000. before do
  2001. Setting.set('ticket_auto_assignment', true)
  2002. Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.by_category(:work_on).pluck(:id) } } })
  2003. end
  2004. it 'does not auto assign' do
  2005. ticket.auto_assign(agent)
  2006. expect(ticket.reload.owner_id).to eq(1)
  2007. end
  2008. end
  2009. end
  2010. end