ticket_spec.rb 88 KB

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