ticket_spec.rb 88 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474
  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. end
  487. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  488. let(:perform) do
  489. {
  490. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  491. 'ticket.action' => { 'value' => 'delete' },
  492. }
  493. end
  494. it 'performs a ticket deletion on a ticket' do
  495. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }
  496. .to change(ticket, :destroyed?).to(true)
  497. end
  498. end
  499. context 'with a "notification.email" trigger' do
  500. # Regression test for https://github.com/zammad/zammad/issues/1543
  501. #
  502. # If a new article fires an email notification trigger,
  503. # and then another article is added to the same ticket
  504. # before that trigger is performed,
  505. # the email template's 'article' var should refer to the originating article,
  506. # not the newest one.
  507. #
  508. # (This occurs whenever one action fires multiple email notification triggers.)
  509. context 'when two articles are created before the trigger fires once (race condition)' do
  510. let!(:article) { create(:ticket_article, ticket: ticket) }
  511. let!(:new_article) { create(:ticket_article, ticket: ticket) }
  512. let(:trigger) do
  513. build(:trigger,
  514. perform: {
  515. 'notification.email' => {
  516. body: '',
  517. recipient: 'ticket_customer',
  518. subject: ''
  519. }
  520. })
  521. end
  522. let(:objects) do
  523. last_article = nil
  524. last_internal_article = nil
  525. last_external_article = nil
  526. all_articles = ticket.articles
  527. if article.nil?
  528. last_article = all_articles.last
  529. last_internal_article = all_articles.reverse.find(&:internal?)
  530. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  531. else
  532. last_article = article
  533. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  534. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  535. end
  536. {
  537. ticket: ticket,
  538. article: last_article,
  539. last_article: last_article,
  540. last_internal_article: last_internal_article,
  541. last_external_article: last_external_article,
  542. created_article: article,
  543. created_internal_article: article&.internal? ? article : nil,
  544. created_external_article: article&.internal? ? nil : article,
  545. }
  546. end
  547. # required by Ticket#perform_changes for email notifications
  548. before { article.ticket.group.update(email_address: create(:email_address)) }
  549. it 'passes the first article to NotificationFactory::Mailer' do
  550. expect(NotificationFactory::Mailer)
  551. .to receive(:template)
  552. .with(hash_including(objects: objects))
  553. .at_least(:once)
  554. .and_call_original
  555. expect(NotificationFactory::Mailer)
  556. .not_to receive(:template)
  557. .with(hash_including(objects: { ticket: ticket, article: new_article }))
  558. ticket.perform_changes(trigger, 'trigger', { article_id: article.id }, 1)
  559. end
  560. end
  561. end
  562. context 'with a notification trigger' do
  563. # https://github.com/zammad/zammad/issues/2782
  564. #
  565. # Notification triggers should log notification as private or public
  566. # according to given configuration
  567. let(:user) { create(:admin, mobile: '+37061010000') }
  568. let(:item) do
  569. {
  570. object: 'Ticket',
  571. object_id: ticket.id,
  572. user_id: user.id,
  573. type: 'update',
  574. article_id: ticket_article.id
  575. }
  576. end
  577. before { ticket.group.users << user }
  578. let(:perform) do
  579. {
  580. notification_key => {
  581. body: 'Old programmers never die. They just branch to a new address.',
  582. recipient: 'ticket_agents',
  583. subject: 'Old programmers never die. They just branch to a new address.'
  584. }
  585. }.deep_merge(additional_options).deep_stringify_keys
  586. end
  587. let(:notification_key) { "notification.#{notification_type}" }
  588. let!(:ticket_article) { create(:ticket_article, ticket: ticket) }
  589. shared_examples 'verify log visibility status' do
  590. shared_examples 'notification trigger' do
  591. it 'adds Ticket::Article' do
  592. expect { ticket.perform_changes(performable, 'trigger', ticket, user) }
  593. .to change { ticket.articles.count }.by(1)
  594. end
  595. it 'new Ticket::Article visibility reflects setting' do
  596. ticket.perform_changes(performable, 'trigger', ticket, User.first)
  597. new_article = ticket.articles.reload.last
  598. expect(new_article.internal).to be target_internal_value
  599. end
  600. end
  601. context 'when set to private' do
  602. let(:additional_options) do
  603. {
  604. notification_key => {
  605. internal: true
  606. }
  607. }
  608. end
  609. let(:target_internal_value) { true }
  610. it_behaves_like 'notification trigger'
  611. end
  612. context 'when set to internal' do
  613. let(:additional_options) do
  614. {
  615. notification_key => {
  616. internal: false
  617. }
  618. }
  619. end
  620. let(:target_internal_value) { false }
  621. it_behaves_like 'notification trigger'
  622. end
  623. context 'when no selection was made' do # ensure previously created triggers default to public
  624. let(:additional_options) do
  625. {}
  626. end
  627. let(:target_internal_value) { false }
  628. it_behaves_like 'notification trigger'
  629. end
  630. end
  631. context 'dispatching email' do
  632. let(:notification_type) { :email }
  633. include_examples 'verify log visibility status'
  634. end
  635. shared_examples 'add a new article' do
  636. it 'adds a new article' do
  637. expect { ticket.perform_changes(performable, 'trigger', item, user) }
  638. .to change { ticket.articles.count }.by(1)
  639. end
  640. end
  641. shared_examples 'add attachment to new article' do
  642. include_examples 'add a new article'
  643. it 'adds attachment to the new article' do
  644. ticket.perform_changes(performable, 'trigger', item, user)
  645. article = ticket.articles.last
  646. expect(article.type.name).to eq('email')
  647. expect(article.sender.name).to eq('System')
  648. expect(article.attachments.count).to eq(1)
  649. expect(article.attachments[0].filename).to eq('some_file.pdf')
  650. expect(article.attachments[0].preferences['Content-ID']).to eq('image/pdf@01CAB192.K8H512Y9')
  651. end
  652. end
  653. shared_examples 'does not add attachment to new article' do
  654. include_examples 'add a new article'
  655. it 'does not add attachment to the new article' do
  656. ticket.perform_changes(performable, 'trigger', item, user)
  657. article = ticket.articles.last
  658. expect(article.type.name).to eq('email')
  659. expect(article.sender.name).to eq('System')
  660. expect(article.attachments.count).to eq(0)
  661. end
  662. end
  663. context 'dispatching email with include attachment present' do
  664. let(:notification_type) { :email }
  665. let(:additional_options) do
  666. {
  667. notification_key => {
  668. include_attachments: 'true'
  669. }
  670. }
  671. end
  672. context 'when ticket has an attachment' do
  673. before do
  674. UserInfo.current_user_id = 1
  675. create(:store,
  676. object: 'Ticket::Article',
  677. o_id: ticket_article.id,
  678. data: 'dGVzdCAxMjM=',
  679. filename: 'some_file.pdf',
  680. preferences: {
  681. 'Content-Type': 'image/pdf',
  682. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  683. })
  684. end
  685. include_examples 'add attachment to new article'
  686. end
  687. context 'when ticket does not have an attachment' do
  688. include_examples 'does not add attachment to new article'
  689. end
  690. end
  691. context 'dispatching email with include attachment not present' do
  692. let(:notification_type) { :email }
  693. let(:additional_options) do
  694. {
  695. notification_key => {
  696. include_attachments: 'false'
  697. }
  698. }
  699. end
  700. context 'when ticket has an attachment' do
  701. before do
  702. UserInfo.current_user_id = 1
  703. create(:store,
  704. object: 'Ticket::Article',
  705. o_id: ticket_article.id,
  706. data: 'dGVzdCAxMjM=',
  707. filename: 'some_file.pdf',
  708. preferences: {
  709. 'Content-Type': 'image/pdf',
  710. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  711. })
  712. end
  713. include_examples 'does not add attachment to new article'
  714. end
  715. context 'when ticket does not have an attachment' do
  716. include_examples 'does not add attachment to new article'
  717. end
  718. end
  719. context 'dispatching SMS' do
  720. let(:notification_type) { :sms }
  721. before { create(:channel, area: 'Sms::Notification') }
  722. include_examples 'verify log visibility status'
  723. end
  724. end
  725. context 'with a "notification.webhook" trigger', performs_jobs: true do
  726. let(:webhook) { create(:webhook, endpoint: 'http://api.example.com/webhook', signature_token: '53CR3t') }
  727. let(:trigger) do
  728. create(:trigger,
  729. perform: {
  730. 'notification.webhook' => { 'webhook_id' => webhook.id }
  731. })
  732. end
  733. it 'schedules the webhooks notification job' do
  734. expect { ticket.perform_changes(trigger, 'trigger', {}, 1) }.to have_enqueued_job(TriggerWebhookJob).with(trigger, ticket, nil)
  735. end
  736. end
  737. context 'Allow placeholders in trigger perform actions for ticket/custom attributes #4216' do
  738. let(:customer) { create(:customer, mobile: '+491907655431') }
  739. let(:ticket) { create(:ticket, customer: customer) }
  740. let(:perform) do
  741. {
  742. 'ticket.title' => {
  743. 'value' => ticket.customer.mobile.to_s,
  744. }
  745. }
  746. end
  747. it 'does replace custom fields in trigger' do
  748. ticket.perform_changes(performable, 'trigger', ticket, User.first)
  749. expect(ticket.reload.title).to eq(customer.mobile)
  750. end
  751. end
  752. end
  753. describe '#trigger_based_notification?' do
  754. let(:ticket) { create(:ticket) }
  755. context 'with a normal user' do
  756. let(:customer) { create(:customer) }
  757. it 'send trigger base notification' do
  758. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  759. end
  760. end
  761. context 'with a permanent failed user' do
  762. let(:failed_date) { 1.second.ago }
  763. let(:customer) do
  764. user = create(:customer)
  765. user.preferences.merge!(mail_delivery_failed: true, mail_delivery_failed_data: failed_date)
  766. user.save!
  767. user
  768. end
  769. it 'send no trigger base notification' do
  770. expect(ticket.send(:trigger_based_notification?, customer)).to be(false)
  771. expect(customer.reload.preferences[:mail_delivery_failed]).to be(true)
  772. expect(customer.preferences[:mail_delivery_failed_data]).to eq(failed_date)
  773. end
  774. context 'with failed date 61 days ago' do
  775. let(:failed_date) { 61.days.ago }
  776. it 'send trigger base notification' do
  777. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  778. expect(customer.reload.preferences[:mail_delivery_failed]).to be(false)
  779. expect(customer.preferences[:mail_delivery_failed_data]).to be_nil
  780. end
  781. end
  782. context 'with failed date 70 days ago' do
  783. let(:failed_date) { 70.days.ago }
  784. it 'send trigger base notification' do
  785. expect(ticket.send(:trigger_based_notification?, customer)).to be(true)
  786. expect(customer.reload.preferences[:mail_delivery_failed]).to be(false)
  787. expect(customer.preferences[:mail_delivery_failed_data]).to be_nil
  788. end
  789. end
  790. end
  791. end
  792. describe '#subject_build' do
  793. context 'with default "ticket_hook_position" setting ("right")' do
  794. it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
  795. expect(ticket.subject_build('foo'))
  796. .to eq("foo [Ticket##{ticket.number}]")
  797. end
  798. context 'and a non-default value for the "ticket_hook" setting' do
  799. before { Setting.set('ticket_hook', 'bar baz') }
  800. it 'replaces "Ticket#" with the new ticket hook' do
  801. expect(ticket.subject_build('foo'))
  802. .to eq("foo [bar baz#{ticket.number}]")
  803. end
  804. end
  805. context 'and a non-default value for the "ticket_hook_divider" setting' do
  806. before { Setting.set('ticket_hook_divider', ': ') }
  807. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  808. expect(ticket.subject_build('foo'))
  809. .to eq("foo [Ticket#: #{ticket.number}]")
  810. end
  811. end
  812. context 'when the given string already contains a ticket reference, but in the wrong place' do
  813. it 'moves the ticket reference to the end' do
  814. expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
  815. .to eq("foo [Ticket##{ticket.number}]")
  816. end
  817. end
  818. context 'when the given string already contains an alternately formatted ticket reference' do
  819. it 'reformats the ticket reference' do
  820. expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
  821. .to eq("foo [Ticket##{ticket.number}]")
  822. end
  823. end
  824. end
  825. context 'with alternate "ticket_hook_position" setting ("left")' do
  826. before { Setting.set('ticket_hook_position', 'left') }
  827. it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
  828. expect(ticket.subject_build('foo'))
  829. .to eq("[Ticket##{ticket.number}] foo")
  830. end
  831. context 'and a non-default value for the "ticket_hook" setting' do
  832. before { Setting.set('ticket_hook', 'bar baz') }
  833. it 'replaces "Ticket#" with the new ticket hook' do
  834. expect(ticket.subject_build('foo'))
  835. .to eq("[bar baz#{ticket.number}] foo")
  836. end
  837. end
  838. context 'and a non-default value for the "ticket_hook_divider" setting' do
  839. before { Setting.set('ticket_hook_divider', ': ') }
  840. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  841. expect(ticket.subject_build('foo'))
  842. .to eq("[Ticket#: #{ticket.number}] foo")
  843. end
  844. end
  845. context 'when the given string already contains a ticket reference, but in the wrong place' do
  846. it 'moves the ticket reference to the start' do
  847. expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
  848. .to eq("[Ticket##{ticket.number}] foo")
  849. end
  850. end
  851. context 'when the given string already contains an alternately formatted ticket reference' do
  852. it 'reformats the ticket reference' do
  853. expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
  854. .to eq("[Ticket##{ticket.number}] foo")
  855. end
  856. end
  857. end
  858. end
  859. describe '#last_original_update_at' do
  860. let(:result) { ticket.last_original_update_at }
  861. it 'returns initial customer enquiry time when customer contacted repeatedly' do
  862. ticket
  863. target = create(:ticket_article, :inbound_email, ticket: ticket)
  864. travel 10.minutes
  865. create(:ticket_article, :inbound_email, ticket: ticket)
  866. expect(result).to eq target.created_at
  867. end
  868. it 'returns agent contact time when customer did not respond to agent reach out' do
  869. ticket
  870. create(:ticket_article, :outbound_email, ticket: ticket)
  871. expect(result).to eq ticket.last_contact_agent_at
  872. end
  873. it 'returns nil if no customer response' do
  874. ticket
  875. expect(result).to be_nil
  876. end
  877. context 'with customer enquiry and agent response' do
  878. before do
  879. ticket
  880. create(:ticket_article, :inbound_email, ticket: ticket)
  881. travel 10.minutes
  882. create(:ticket_article, :outbound_email, ticket: ticket)
  883. travel 10.minutes
  884. end
  885. it 'returns last customer enquiry time when agent did not respond yet' do
  886. target = create(:ticket_article, :inbound_email, ticket: ticket)
  887. expect(result).to eq target.created_at
  888. end
  889. it 'returns agent response time when agent responded to customer enquiry' do
  890. expect(result).to eq ticket.last_contact_agent_at
  891. end
  892. end
  893. end
  894. describe '#param_cleanup' do
  895. it 'does only remove parameters which are invalid and not the complete params hash if one element is invalid (#3743)' do
  896. expect(described_class.param_cleanup({ state_id: 3, customer_id: 'guess:1234' }, true, false, false)).to eq({ 'state_id' => 3 })
  897. end
  898. end
  899. end
  900. describe 'Attributes:' do
  901. describe '#owner' do
  902. let(:original_owner) { create(:agent, groups: [ticket.group]) }
  903. before { ticket.update(owner: original_owner) }
  904. context 'when assigned directly' do
  905. context 'to an active agent belonging to ticket.group' do
  906. let(:agent) { create(:agent, groups: [ticket.group]) }
  907. it 'can be set' do
  908. expect { ticket.update(owner: agent) }
  909. .to change { ticket.reload.owner }.to(agent)
  910. end
  911. end
  912. context 'to an agent not belonging to ticket.group' do
  913. let(:agent) { create(:agent, groups: [other_group]) }
  914. let(:other_group) { create(:group) }
  915. it 'resets to default user (id: 1) instead' do
  916. expect { ticket.update(owner: agent) }
  917. .to change { ticket.reload.owner }.to(User.first)
  918. end
  919. end
  920. context 'to an inactive agent' do
  921. let(:agent) { create(:agent, groups: [ticket.group], active: false) }
  922. it 'resets to default user (id: 1) instead' do
  923. expect { ticket.update(owner: agent) }
  924. .to change { ticket.reload.owner }.to(User.first)
  925. end
  926. end
  927. context 'to a non-agent' do
  928. let(:agent) { create(:customer, groups: [ticket.group]) }
  929. it 'resets to default user (id: 1) instead' do
  930. expect { ticket.update(owner: agent) }
  931. .to change { ticket.reload.owner }.to(User.first)
  932. end
  933. end
  934. end
  935. context 'when the ticket is updated for any other reason' do
  936. context 'if original owner is still an active agent belonging to ticket.group' do
  937. it 'does not change' do
  938. expect { create(:ticket_article, ticket: ticket) }
  939. .not_to change { ticket.reload.owner }
  940. end
  941. end
  942. context 'if original owner has left ticket.group' do
  943. before { original_owner.groups = [] }
  944. it 'resets to default user (id: 1)' do
  945. expect { create(:ticket_article, ticket: ticket) }
  946. .to change { ticket.reload.owner }.to(User.first)
  947. end
  948. end
  949. context 'if original owner has become inactive' do
  950. before { original_owner.update(active: false) }
  951. it 'resets to default user (id: 1)' do
  952. expect { create(:ticket_article, ticket: ticket) }
  953. .to change { ticket.reload.owner }.to(User.first)
  954. end
  955. end
  956. context 'if original owner has lost agent status' do
  957. before { original_owner.roles = [create(:role)] }
  958. it 'resets to default user (id: 1)' do
  959. Rails.cache.clear
  960. expect { create(:ticket_article, ticket: ticket) }
  961. .to change { ticket.reload.owner }.to(User.first)
  962. end
  963. end
  964. context 'when the Ticket is closed' do
  965. before do
  966. ticket.update!(state: Ticket::State.lookup(name: 'closed'))
  967. end
  968. context 'if original owner is still an active agent belonging to ticket.group' do
  969. it 'does not change' do
  970. expect { create(:ticket_article, ticket: ticket) }
  971. .not_to change { ticket.reload.owner }
  972. end
  973. end
  974. context 'if original owner has left ticket.group' do
  975. before { original_owner.groups = [] }
  976. it 'does not change' do
  977. expect { create(:ticket_article, ticket: ticket) }
  978. .not_to change { ticket.reload.owner }
  979. end
  980. end
  981. context 'if original owner has become inactive' do
  982. before { original_owner.update(active: false) }
  983. it 'does not change' do
  984. expect { create(:ticket_article, ticket: ticket) }
  985. .not_to change { ticket.reload.owner }
  986. end
  987. end
  988. context 'if original owner has lost agent status' do
  989. before { original_owner.roles = [create(:role)] }
  990. it 'does not change' do
  991. expect { create(:ticket_article, ticket: ticket) }
  992. .not_to change { ticket.reload.owner }
  993. end
  994. end
  995. end
  996. end
  997. end
  998. describe '#state' do
  999. context 'when originally "new" (default)' do
  1000. context 'and a customer article is added' do
  1001. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
  1002. it 'stays "new"' do
  1003. expect { article }
  1004. .not_to change { ticket.state.name }.from('new')
  1005. end
  1006. end
  1007. context 'and a non-customer article is added' do
  1008. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1009. it 'switches to "open"' do
  1010. expect { article }
  1011. .to change { ticket.reload.state.name }.from('new').to('open')
  1012. end
  1013. end
  1014. end
  1015. context 'when originally "closed"' do
  1016. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  1017. context 'when a non-customer article is added' do
  1018. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1019. it 'stays "closed"' do
  1020. expect { article }.not_to change { ticket.reload.state.name }
  1021. end
  1022. end
  1023. end
  1024. end
  1025. describe '#pending_time' do
  1026. subject(:ticket) { create(:ticket, pending_time: 2.days.from_now) }
  1027. context 'when #state is updated to any non-"pending" value' do
  1028. it 'is reset to nil' do
  1029. expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
  1030. .to change(ticket, :pending_time).to(nil)
  1031. end
  1032. end
  1033. # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
  1034. context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
  1035. it 'does not prematurely raise within the callback (#reset_pending_time)' do
  1036. expect { ticket.update!(state: nil) }
  1037. .to raise_error(ActiveRecord::StatementInvalid)
  1038. end
  1039. end
  1040. end
  1041. describe '#escalation_at' do
  1042. before { freeze_time } # freeze time
  1043. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1044. let(:calendar) { create(:calendar, :'24/7') }
  1045. context 'with no SLAs in the system' do
  1046. it 'defaults to nil' do
  1047. expect(ticket.escalation_at).to be_nil
  1048. end
  1049. end
  1050. context 'with an SLA in the system' do
  1051. before { sla } # create sla
  1052. it 'is set based on SLA’s #first_response_time' do
  1053. expect(ticket.reload.escalation_at.to_i)
  1054. .to eq(1.hour.from_now.to_i)
  1055. end
  1056. context 'after first agent’s response' do
  1057. before { ticket } # create ticket
  1058. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1059. it 'is updated based on the SLA’s #close_escalation_at' do
  1060. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  1061. expect { article }
  1062. .to change { ticket.reload.escalation_at }
  1063. .to(ticket.reload.close_escalation_at)
  1064. end
  1065. context 'when new #update_time is later than original #solution_time' do
  1066. it 'is updated based on the original #solution_time' do
  1067. travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  1068. expect { article }
  1069. .to change { ticket.reload.escalation_at }
  1070. .to(4.hours.after(ticket.created_at))
  1071. end
  1072. end
  1073. end
  1074. end
  1075. context 'when updated after an SLA has been added to the system' do
  1076. before do
  1077. ticket # create ticket
  1078. sla # create sla
  1079. end
  1080. it 'is updated based on the new SLA’s #first_response_time' do
  1081. expect { ticket.save! }
  1082. .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
  1083. end
  1084. end
  1085. context 'when updated after all SLAs have been removed from the system' do
  1086. before do
  1087. sla # create sla
  1088. ticket # create ticket
  1089. sla.destroy
  1090. end
  1091. it 'is set to nil' do
  1092. expect { ticket.save! }
  1093. .to change { ticket.reload.escalation_at }.to(nil)
  1094. end
  1095. end
  1096. context 'when within last (relative)' do
  1097. let(:first_response_time) { 5 }
  1098. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1099. let(:within_condition) do
  1100. { 'ticket.escalation_at'=>{ 'operator' => 'within last (relative)', 'value' => '30', 'range' => 'minute' } }
  1101. end
  1102. before do
  1103. sla
  1104. travel_to '2020-11-05 11:37:00'
  1105. ticket = create(:ticket)
  1106. create(:ticket_article, :inbound_email, ticket: ticket)
  1107. travel_to '2020-11-05 11:50:00'
  1108. end
  1109. context 'when in range' do
  1110. it 'does find the ticket' do
  1111. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1112. expect(count).to eq(1)
  1113. end
  1114. end
  1115. context 'when out of range' do
  1116. let(:first_response_time) { 500 }
  1117. it 'does not find the ticket' do
  1118. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1119. expect(count).to eq(0)
  1120. end
  1121. end
  1122. end
  1123. context 'when till (relative)' do
  1124. let(:first_response_time) { 5 }
  1125. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1126. let(:condition) do
  1127. { 'ticket.escalation_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }
  1128. end
  1129. before do
  1130. sla
  1131. travel_to '2020-11-05 11:37:00'
  1132. ticket = create(:ticket)
  1133. create(:ticket_article, :inbound_email, ticket: ticket)
  1134. travel_to '2020-11-05 11:50:00'
  1135. end
  1136. context 'when in range' do
  1137. it 'does find the ticket' do
  1138. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1139. expect(count).to eq(1)
  1140. end
  1141. end
  1142. context 'when out of range' do
  1143. let(:first_response_time) { 500 }
  1144. it 'does not find the ticket' do
  1145. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1146. expect(count).to eq(0)
  1147. end
  1148. end
  1149. end
  1150. context 'when from (relative)' do
  1151. let(:first_response_time) { 5 }
  1152. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1153. let(:condition) do
  1154. { 'ticket.escalation_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } }
  1155. end
  1156. before do
  1157. sla
  1158. travel_to '2020-11-05 11:37:00'
  1159. ticket = create(:ticket)
  1160. create(:ticket_article, :inbound_email, ticket: ticket)
  1161. end
  1162. context 'when in range' do
  1163. it 'does find the ticket' do
  1164. travel_to '2020-11-05 11:50:00'
  1165. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1166. expect(count).to eq(1)
  1167. end
  1168. end
  1169. context 'when out of range' do
  1170. let(:first_response_time) { 5 }
  1171. it 'does not find the ticket' do
  1172. travel_to '2020-11-05 13:50:00'
  1173. count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
  1174. expect(count).to eq(0)
  1175. end
  1176. end
  1177. end
  1178. context 'when within next (relative)' do
  1179. let(:first_response_time) { 5 }
  1180. let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
  1181. let(:within_condition) do
  1182. { 'ticket.escalation_at'=>{ 'operator' => 'within next (relative)', 'value' => '30', 'range' => 'minute' } }
  1183. end
  1184. before do
  1185. sla
  1186. travel_to '2020-11-05 11:50:00'
  1187. ticket = create(:ticket)
  1188. create(:ticket_article, :inbound_email, ticket: ticket)
  1189. travel_to '2020-11-05 11:37:00'
  1190. end
  1191. context 'when in range' do
  1192. it 'does find the ticket' do
  1193. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1194. expect(count).to eq(1)
  1195. end
  1196. end
  1197. context 'when out of range' do
  1198. let(:first_response_time) { 500 }
  1199. it 'does not find the ticket' do
  1200. count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
  1201. expect(count).to eq(0)
  1202. end
  1203. end
  1204. end
  1205. end
  1206. describe '#first_response_escalation_at' do
  1207. before { freeze_time } # freeze time
  1208. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1209. let(:calendar) { create(:calendar, :'24/7') }
  1210. context 'with no SLAs in the system' do
  1211. it 'defaults to nil' do
  1212. expect(ticket.first_response_escalation_at).to be_nil
  1213. end
  1214. end
  1215. context 'with an SLA in the system' do
  1216. before { sla } # create sla
  1217. it 'is set based on SLA’s #first_response_time' do
  1218. expect(ticket.reload.first_response_escalation_at.to_i)
  1219. .to eq(1.hour.from_now.to_i)
  1220. end
  1221. context 'after first agent’s response' do
  1222. before { ticket } # create ticket
  1223. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1224. it 'is cleared' do
  1225. expect { article }.to change { ticket.reload.first_response_escalation_at }.to(nil)
  1226. end
  1227. end
  1228. end
  1229. end
  1230. describe '#update_escalation_at' do
  1231. before { freeze_time } # freeze time
  1232. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1233. let(:calendar) { create(:calendar, :'24/7') }
  1234. context 'with no SLAs in the system' do
  1235. it 'defaults to nil' do
  1236. expect(ticket.update_escalation_at).to be_nil
  1237. end
  1238. end
  1239. context 'with an SLA in the system' do
  1240. before { sla } # create sla
  1241. it 'is set based on SLA’s #update_time' do
  1242. travel 1.minute
  1243. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  1244. expect(ticket.reload.update_escalation_at.to_i)
  1245. .to eq(3.hours.from_now.to_i)
  1246. end
  1247. context 'after first agent’s response' do
  1248. before { ticket } # create ticket
  1249. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1250. it 'is updated based on the SLA’s #update_time' do
  1251. create(:ticket_article, ticket: ticket, sender_name: 'Customer')
  1252. travel(1.minute)
  1253. expect { article }
  1254. .to change { ticket.reload.update_escalation_at }
  1255. .to(nil)
  1256. end
  1257. end
  1258. end
  1259. end
  1260. describe '#close_escalation_at' do
  1261. before { freeze_time } # freeze time
  1262. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) }
  1263. let(:calendar) { create(:calendar, :'24/7') }
  1264. context 'with no SLAs in the system' do
  1265. it 'defaults to nil' do
  1266. expect(ticket.close_escalation_at).to be_nil
  1267. end
  1268. end
  1269. context 'with an SLA in the system' do
  1270. before { sla } # create sla
  1271. it 'is set based on SLA’s #solution_time' do
  1272. expect(ticket.reload.close_escalation_at.to_i)
  1273. .to eq(4.hours.from_now.to_i)
  1274. end
  1275. context 'after first agent’s response' do
  1276. before { ticket } # create ticket
  1277. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  1278. it 'does not change' do
  1279. expect { article }.not_to change(ticket, :close_escalation_at)
  1280. end
  1281. end
  1282. end
  1283. end
  1284. end
  1285. describe '.search' do
  1286. shared_examples 'search permissions' do
  1287. let(:group) { create(:group) }
  1288. before do
  1289. ticket
  1290. end
  1291. shared_examples 'permitted' do
  1292. it 'finds Ticket' do
  1293. expect(described_class.search(query: ticket.number, current_user: current_user).count).to eq(1)
  1294. end
  1295. end
  1296. shared_examples 'no permission' do
  1297. it "doesn't find Ticket" do
  1298. expect(described_class.search(query: ticket.number, current_user: current_user)).to be_blank
  1299. end
  1300. end
  1301. context 'Agent with Group access' do
  1302. let(:ticket) do
  1303. ticket = create(:ticket, group: group)
  1304. create(:ticket_article, ticket: ticket)
  1305. ticket
  1306. end
  1307. let(:current_user) { create(:agent, groups: [group]) }
  1308. it_behaves_like 'permitted'
  1309. end
  1310. context 'when Agent is Customer of Ticket' do
  1311. let(:ticket) do
  1312. ticket = create(:ticket, customer: current_user)
  1313. create(:ticket_article, ticket: ticket)
  1314. ticket
  1315. end
  1316. let(:current_user) { create(:agent_and_customer) }
  1317. it_behaves_like 'permitted'
  1318. end
  1319. context 'for Organization access' do
  1320. let(:ticket) do
  1321. ticket = create(:ticket, customer: customer)
  1322. create(:ticket_article, ticket: ticket)
  1323. ticket
  1324. end
  1325. let(:customer) { create(:customer, organization: organization) }
  1326. context 'when Organization is shared' do
  1327. let(:organization) { create(:organization, shared: true) }
  1328. context 'for unrelated Agent' do
  1329. let(:current_user) { create(:agent) }
  1330. it_behaves_like 'no permission'
  1331. end
  1332. context 'for Agent in same Organization' do
  1333. let(:current_user) { create(:agent_and_customer, organization: organization) }
  1334. it_behaves_like 'permitted'
  1335. end
  1336. context 'for Customer of Ticket' do
  1337. let(:current_user) { customer }
  1338. it_behaves_like 'permitted'
  1339. end
  1340. end
  1341. context 'when Organization is not shared' do
  1342. let(:organization) { create(:organization, shared: false) }
  1343. context 'for unrelated Agent' do
  1344. let(:current_user) { create(:agent) }
  1345. it_behaves_like 'no permission'
  1346. end
  1347. context 'for Agent in same Organization' do
  1348. let(:current_user) { create(:agent_and_customer, organization: organization) }
  1349. it_behaves_like 'no permission'
  1350. end
  1351. context 'for Customer of Ticket' do
  1352. let(:current_user) { customer }
  1353. it_behaves_like 'permitted'
  1354. end
  1355. end
  1356. end
  1357. end
  1358. context 'with searchindex', searchindex: true do
  1359. include_examples 'search permissions' do
  1360. before do
  1361. searchindex_model_reload([described_class])
  1362. end
  1363. end
  1364. end
  1365. context 'without searchindex' do
  1366. before do
  1367. Setting.set('es_url', nil)
  1368. end
  1369. include_examples 'search permissions'
  1370. end
  1371. end
  1372. describe 'Callbacks & Observers -' do
  1373. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  1374. it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
  1375. expect { create(:ticket, title: "some title \u0000 123") }
  1376. .not_to raise_error
  1377. end
  1378. end
  1379. describe 'XSS protection:' do
  1380. subject(:ticket) { create(:ticket, title: title) }
  1381. let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
  1382. it 'does not sanitize title' do
  1383. expect(ticket.title).to eq(title)
  1384. end
  1385. end
  1386. describe 'Cti::CallerId syncing:', performs_jobs: true do
  1387. subject(:ticket) { build(:ticket) }
  1388. before { allow(Cti::CallerId).to receive(:build) }
  1389. it 'adds numbers in article bodies (via Cti::CallerId.build)' do
  1390. expect(Cti::CallerId).to receive(:build).with(ticket)
  1391. ticket.save
  1392. perform_enqueued_jobs commit_transaction: true
  1393. end
  1394. end
  1395. describe 'Touching associations on update:' do
  1396. subject(:ticket) { create(:ticket, customer: customer) }
  1397. let(:customer) { create(:customer, organization: organization) }
  1398. let(:organization) { create(:organization) }
  1399. let(:other_customer) { create(:customer, organization: other_organization) }
  1400. let(:other_organization) { create(:organization) }
  1401. context 'on creation' do
  1402. it 'touches its customer and his organization' do
  1403. expect { ticket }
  1404. .to change { customer.reload.updated_at }
  1405. .and change { organization.reload.updated_at }
  1406. end
  1407. end
  1408. context 'on destruction' do
  1409. before { ticket }
  1410. it 'touches its customer and his organization' do
  1411. expect { ticket.destroy }
  1412. .to change { customer.reload.updated_at }
  1413. .and change { organization.reload.updated_at }
  1414. end
  1415. end
  1416. context 'when customer association is changed' do
  1417. it 'touches both old and new customer, and their organizations' do
  1418. expect { ticket.update(customer: other_customer) }
  1419. .to change { customer.reload.updated_at }
  1420. .and change { organization.reload.updated_at }
  1421. .and change { other_customer.reload.updated_at }
  1422. .and change { other_organization.reload.updated_at }
  1423. end
  1424. end
  1425. end
  1426. describe 'Association & attachment management:' do
  1427. it 'deletes all related ActivityStreams on destroy' do
  1428. create_list(:activity_stream, 3, o: ticket)
  1429. expect { ticket.destroy }
  1430. .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1431. .to(false)
  1432. end
  1433. it 'deletes all related Links on destroy' do
  1434. create(:link, from: ticket, to: create(:ticket))
  1435. create(:link, from: create(:ticket), to: ticket)
  1436. create(:link, from: ticket, to: create(:ticket))
  1437. expect { ticket.destroy }
  1438. .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
  1439. .to(false)
  1440. end
  1441. it 'deletes all related Articles on destroy' do
  1442. create_list(:ticket_article, 3, ticket: ticket)
  1443. expect { ticket.destroy }
  1444. .to change { Ticket::Article.exists?(ticket: ticket) }
  1445. .to(false)
  1446. end
  1447. it 'deletes all related OnlineNotifications on destroy' do
  1448. create_list(:online_notification, 3, o: ticket)
  1449. expect { ticket.destroy }
  1450. .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
  1451. .to(false)
  1452. end
  1453. it 'deletes all related Tags on destroy' do
  1454. create_list(:tag, 3, o: ticket)
  1455. expect { ticket.destroy }
  1456. .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1457. .to(false)
  1458. end
  1459. it 'deletes all related Histories on destroy' do
  1460. create_list(:history, 3, o: ticket)
  1461. expect { ticket.destroy }
  1462. .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  1463. .to(false)
  1464. end
  1465. it 'deletes all related RecentViews on destroy' do
  1466. create_list(:recent_view, 3, o: ticket)
  1467. expect { ticket.destroy }
  1468. .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  1469. .to(false)
  1470. end
  1471. it 'destroys all related dependencies' do
  1472. refs_known = { 'Ticket::Article' => { 'ticket_id'=>1 },
  1473. 'Ticket::TimeAccounting' => { 'ticket_id'=>1 },
  1474. 'Ticket::SharedDraftZoom' => { 'ticket_id'=>0 },
  1475. 'Ticket::Flag' => { 'ticket_id'=>1 } }
  1476. ticket = create(:ticket)
  1477. article = create(:ticket_article, ticket: ticket)
  1478. accounting = create(:ticket_time_accounting, ticket: ticket)
  1479. flag = create(:ticket_flag, ticket: ticket)
  1480. refs_ticket = Models.references('Ticket', ticket.id, true)
  1481. expect(refs_ticket).to eq(refs_known)
  1482. ticket.destroy
  1483. expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1484. expect { article.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1485. expect { accounting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1486. expect { flag.reload }.to raise_exception(ActiveRecord::RecordNotFound)
  1487. end
  1488. context 'when ticket is generated from email (with attachments)' do
  1489. subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
  1490. let(:raw_email) { Rails.root.join('test/data/mail/mail001.box').read }
  1491. it 'adds attachments to the Store{::File,::Provider::DB} tables' do
  1492. expect { ticket }
  1493. .to change(Store, :count).by(2)
  1494. .and change(Store::File, :count).by(2)
  1495. .and change(Store::Provider::DB, :count).by(2)
  1496. end
  1497. context 'and subsequently destroyed' do
  1498. it 'deletes all related attachments' do
  1499. ticket # create ticket
  1500. expect { ticket.destroy }
  1501. .to change(Store, :count).by(-2)
  1502. .and change(Store::File, :count).by(-2)
  1503. .and change(Store::Provider::DB, :count).by(-2)
  1504. end
  1505. end
  1506. context 'and a duplicate ticket is generated from the same email' do
  1507. before { ticket } # create ticket
  1508. let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
  1509. it 'adds duplicate attachments to the Store table only' do
  1510. expect { duplicate }
  1511. .to change(Store, :count).by(2)
  1512. .and not_change(Store::File, :count)
  1513. .and not_change(Store::Provider::DB, :count)
  1514. end
  1515. context 'when only the duplicate ticket is destroyed' do
  1516. it 'deletes only the duplicate attachments' do
  1517. duplicate # create ticket
  1518. expect { duplicate.destroy }
  1519. .to change(Store, :count).by(-2)
  1520. .and not_change(Store::File, :count)
  1521. .and not_change(Store::Provider::DB, :count)
  1522. end
  1523. it 'deletes all related attachments' do
  1524. duplicate.destroy
  1525. expect { ticket.destroy }
  1526. .to change(Store, :count).by(-2)
  1527. .and change(Store::File, :count).by(-2)
  1528. .and change(Store::Provider::DB, :count).by(-2)
  1529. end
  1530. end
  1531. end
  1532. end
  1533. end
  1534. describe 'Ticket lifecycle order-of-operations:', performs_jobs: true do
  1535. subject!(:ticket) { create(:ticket) }
  1536. let!(:agent) { create(:agent, groups: [group]) }
  1537. let(:group) { create(:group) }
  1538. before do
  1539. create(
  1540. :trigger,
  1541. condition: { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } },
  1542. perform: { 'ticket.group_id' => { 'value' => group.id } }
  1543. )
  1544. end
  1545. it 'fires triggers before new ticket notifications are sent' do
  1546. expect { TransactionDispatcher.commit }
  1547. .to change { ticket.reload.group }.to(group)
  1548. expect { perform_enqueued_jobs }
  1549. .to change { NotificationFactory::Mailer.already_sent?(ticket, agent, 'email') }.to(1)
  1550. end
  1551. end
  1552. describe 'Ticket has changed attributes:' do
  1553. subject!(:ticket) { create(:ticket) }
  1554. let(:group) { create(:group) }
  1555. let(:condition_field) { nil }
  1556. shared_examples 'updated ticket group with trigger condition' do
  1557. it 'updated ticket group with has changed trigger condition' do
  1558. expect { TransactionDispatcher.commit }.to change { ticket.reload.group }.to(group)
  1559. end
  1560. end
  1561. before do
  1562. create(
  1563. :trigger,
  1564. condition: { "ticket.#{condition_field}" => { 'operator' => 'has changed', 'value' => 'create' } },
  1565. perform: { 'ticket.group_id' => { 'value' => group.id } }
  1566. )
  1567. ticket.update!(condition_field => Time.zone.now)
  1568. end
  1569. context "when changing 'first_response_at' attribute" do
  1570. let(:condition_field) { 'first_response_at' }
  1571. include_examples 'updated ticket group with trigger condition'
  1572. end
  1573. context "when changing 'close_at' attribute" do
  1574. let(:condition_field) { 'close_at' }
  1575. include_examples 'updated ticket group with trigger condition'
  1576. end
  1577. context "when changing 'last_contact_agent_at' attribute" do
  1578. let(:condition_field) { 'last_contact_agent_at' }
  1579. include_examples 'updated ticket group with trigger condition'
  1580. end
  1581. context "when changing 'last_contact_customer_at' attribute" do
  1582. let(:condition_field) { 'last_contact_customer_at' }
  1583. include_examples 'updated ticket group with trigger condition'
  1584. end
  1585. context "when changing 'last_contact_at' attribute" do
  1586. let(:condition_field) { 'last_contact_at' }
  1587. include_examples 'updated ticket group with trigger condition'
  1588. end
  1589. end
  1590. end
  1591. describe 'Mentions:', sends_notification_emails: true do
  1592. context 'when notifications', performs_jobs: true do
  1593. let(:prefs_matrix_no_mentions) do
  1594. { 'notification_config' =>
  1595. { 'matrix' =>
  1596. { 'create' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1597. 'update' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
  1598. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1599. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1600. end
  1601. let(:prefs_matrix_only_mentions) do
  1602. { 'notification_config' =>
  1603. { 'matrix' =>
  1604. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1605. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1606. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1607. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
  1608. end
  1609. let(:prefs_matrix_only_mentions_groups) do
  1610. { 'notification_config' =>
  1611. { 'matrix' =>
  1612. { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1613. 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
  1614. 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
  1615. 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } },
  1616. 'group_ids' => [create(:group).id, create(:group).id, create(:group).id] } }
  1617. end
  1618. let(:mention_group) { create(:group) }
  1619. let(:no_access_group) { create(:group) }
  1620. let(:user_only_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions) }
  1621. let(:user_read_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions_groups) }
  1622. let(:user_no_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_no_mentions) }
  1623. let(:ticket) { create(:ticket, group: mention_group, owner: user_no_mentions) }
  1624. it 'does inform mention user about the ticket update' do
  1625. create(:mention, mentionable: ticket, user: user_only_mentions)
  1626. create(:mention, mentionable: ticket, user: user_read_mentions)
  1627. create(:mention, mentionable: ticket, user: user_no_mentions)
  1628. perform_enqueued_jobs commit_transaction: true
  1629. check_notification do
  1630. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1631. perform_enqueued_jobs commit_transaction: true
  1632. sent(
  1633. template: 'ticket_update',
  1634. user: user_no_mentions,
  1635. )
  1636. sent(
  1637. template: 'ticket_update',
  1638. user: user_read_mentions,
  1639. )
  1640. sent(
  1641. template: 'ticket_update',
  1642. user: user_only_mentions,
  1643. )
  1644. end
  1645. end
  1646. it 'does not inform mention user about the ticket update' do
  1647. ticket
  1648. perform_enqueued_jobs commit_transaction: true
  1649. check_notification do
  1650. ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
  1651. perform_enqueued_jobs commit_transaction: true
  1652. sent(
  1653. template: 'ticket_update',
  1654. user: user_no_mentions,
  1655. )
  1656. not_sent(
  1657. template: 'ticket_update',
  1658. user: user_read_mentions,
  1659. )
  1660. not_sent(
  1661. template: 'ticket_update',
  1662. user: user_only_mentions,
  1663. )
  1664. end
  1665. end
  1666. it 'does inform mention user about ticket creation' do
  1667. check_notification do
  1668. ticket = create(:ticket, owner: user_no_mentions, group: mention_group)
  1669. create(:mention, mentionable: ticket, user: user_read_mentions)
  1670. create(:mention, mentionable: ticket, user: user_only_mentions)
  1671. perform_enqueued_jobs commit_transaction: true
  1672. sent(
  1673. template: 'ticket_create',
  1674. user: user_no_mentions,
  1675. )
  1676. sent(
  1677. template: 'ticket_create',
  1678. user: user_read_mentions,
  1679. )
  1680. sent(
  1681. template: 'ticket_create',
  1682. user: user_only_mentions,
  1683. )
  1684. end
  1685. end
  1686. it 'does not inform mention user about ticket creation' do
  1687. check_notification do
  1688. create(:ticket, owner: user_no_mentions, group: mention_group)
  1689. perform_enqueued_jobs commit_transaction: true
  1690. sent(
  1691. template: 'ticket_create',
  1692. user: user_no_mentions,
  1693. )
  1694. not_sent(
  1695. template: 'ticket_create',
  1696. user: user_read_mentions,
  1697. )
  1698. not_sent(
  1699. template: 'ticket_create',
  1700. user: user_only_mentions,
  1701. )
  1702. end
  1703. end
  1704. it 'does not inform mention user about ticket creation because of no permissions' do
  1705. check_notification do
  1706. ticket = create(:ticket, group: no_access_group)
  1707. build(:mention, mentionable: ticket, user: user_read_mentions).save!(validate: false)
  1708. build(:mention, mentionable: ticket, user: user_only_mentions).save!(validate: false)
  1709. perform_enqueued_jobs commit_transaction: true
  1710. not_sent(
  1711. template: 'ticket_create',
  1712. user: user_read_mentions,
  1713. )
  1714. not_sent(
  1715. template: 'ticket_create',
  1716. user: user_only_mentions,
  1717. )
  1718. end
  1719. end
  1720. end
  1721. context 'selectors' do
  1722. let(:mention_group) { create(:group) }
  1723. let(:ticket_mentions) { create(:ticket, group: mention_group) }
  1724. let(:ticket_normal) { create(:ticket, group: mention_group) }
  1725. let(:user_mentions) { create(:agent, groups: [mention_group]) }
  1726. let(:user_no_mentions) { create(:agent, groups: [mention_group]) }
  1727. before do
  1728. described_class.destroy_all
  1729. ticket_normal
  1730. user_no_mentions
  1731. create(:mention, mentionable: ticket_mentions, user: user_mentions)
  1732. end
  1733. it 'pre condition is not_set' do
  1734. condition = {
  1735. 'ticket.mention_user_ids' => {
  1736. pre_condition: 'not_set',
  1737. operator: 'is',
  1738. },
  1739. }
  1740. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1741. .to match_array([1, [ticket_normal].to_a])
  1742. end
  1743. it 'pre condition is not not_set' do
  1744. condition = {
  1745. 'ticket.mention_user_ids' => {
  1746. pre_condition: 'not_set',
  1747. operator: 'is not',
  1748. },
  1749. }
  1750. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1751. .to match_array([1, [ticket_mentions].to_a])
  1752. end
  1753. it 'pre condition is current_user.id' do
  1754. condition = {
  1755. 'ticket.mention_user_ids' => {
  1756. pre_condition: 'current_user.id',
  1757. operator: 'is',
  1758. },
  1759. }
  1760. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1761. .to match_array([1, [ticket_mentions].to_a])
  1762. end
  1763. it 'pre condition is not current_user.id' do
  1764. condition = {
  1765. 'ticket.mention_user_ids' => {
  1766. pre_condition: 'current_user.id',
  1767. operator: 'is not',
  1768. },
  1769. }
  1770. expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
  1771. .to match_array([0, []])
  1772. end
  1773. it 'pre condition is specific' do
  1774. condition = {
  1775. 'ticket.mention_user_ids' => {
  1776. pre_condition: 'specific',
  1777. operator: 'is',
  1778. value: user_mentions.id
  1779. },
  1780. }
  1781. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1782. .to match_array([1, [ticket_mentions].to_a])
  1783. end
  1784. it 'pre condition is not specific' do
  1785. condition = {
  1786. 'ticket.mention_user_ids' => {
  1787. pre_condition: 'specific',
  1788. operator: 'is not',
  1789. value: user_mentions.id
  1790. },
  1791. }
  1792. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  1793. .to match_array([0, []])
  1794. end
  1795. end
  1796. end
  1797. describe '.search_index_attribute_lookup_oversized?' do
  1798. subject!(:ticket) { create(:ticket) }
  1799. context 'when payload is ok' do
  1800. let(:current_payload_size) { 3.megabyte }
  1801. it 'return false' do
  1802. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be false
  1803. end
  1804. end
  1805. context 'when payload is bigger' do
  1806. let(:current_payload_size) { 350.megabyte }
  1807. it 'return true' do
  1808. expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be true
  1809. end
  1810. end
  1811. end
  1812. describe '.search_index_attribute_lookup_file_oversized?' do
  1813. subject!(:store) do
  1814. create(:store,
  1815. object: 'SomeObject',
  1816. o_id: 1,
  1817. data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
  1818. filename: 'test.TXT')
  1819. end
  1820. context 'when total payload is ok' do
  1821. let(:current_payload_size) { 200.megabyte }
  1822. it 'return false' do
  1823. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be false
  1824. end
  1825. end
  1826. context 'when total payload is oversized' do
  1827. let(:current_payload_size) { 299.megabyte }
  1828. it 'return true' do
  1829. expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be true
  1830. end
  1831. end
  1832. end
  1833. describe '.search_index_attribute_lookup_file_ignored?' do
  1834. context 'when attachment is indexable' do
  1835. subject!(:store_with_indexable_extention) do
  1836. create(:store,
  1837. object: 'SomeObject',
  1838. o_id: 1,
  1839. data: 'some content',
  1840. filename: 'test.TXT')
  1841. end
  1842. it 'return false' do
  1843. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_with_indexable_extention)).to be false
  1844. end
  1845. end
  1846. context 'when attachment is no indexable' do
  1847. subject!(:store_without_indexable_extention) do
  1848. create(:store,
  1849. object: 'SomeObject',
  1850. o_id: 1,
  1851. data: 'some content',
  1852. filename: 'test.BIN')
  1853. end
  1854. it 'return true' do
  1855. expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_without_indexable_extention)).to be true
  1856. end
  1857. end
  1858. end
  1859. describe '.search_index_attribute_lookup' do
  1860. subject!(:ticket) { create(:ticket) }
  1861. let(:search_index_attribute_lookup) do
  1862. article1 = create(:ticket_article, ticket: ticket)
  1863. create(:store,
  1864. object: 'Ticket::Article',
  1865. o_id: article1.id,
  1866. data: 'some content',
  1867. filename: 'some_file.bin',
  1868. preferences: {
  1869. 'Content-Type' => 'text/plain',
  1870. })
  1871. create(:store,
  1872. object: 'Ticket::Article',
  1873. o_id: article1.id,
  1874. data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
  1875. filename: 'some_file.pdf',
  1876. preferences: {
  1877. 'Content-Type' => 'image/pdf',
  1878. })
  1879. create(:store,
  1880. object: 'Ticket::Article',
  1881. o_id: article1.id,
  1882. data: 'a' * ((1024**2) * 5.8), # with 5,8 mb
  1883. filename: 'some_file.txt',
  1884. preferences: {
  1885. 'Content-Type' => 'text/plain',
  1886. })
  1887. create(:ticket_article, ticket: ticket, body: 'a' * ((1024**2) * 1.2)) # body with 1,2 mb
  1888. create(:ticket_article, ticket: ticket)
  1889. ticket.search_index_attribute_lookup
  1890. end
  1891. context 'when es_attachment_max_size_in_mb takes all attachments' do
  1892. before { Setting.set('es_attachment_max_size_in_mb', 15) }
  1893. it 'verify count of articles' do
  1894. expect(search_index_attribute_lookup['article'].count).to eq 3
  1895. end
  1896. it 'verify count of attachments' do
  1897. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 2
  1898. end
  1899. it 'verify if pdf exists' do
  1900. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1901. end
  1902. it 'verify if txt exists' do
  1903. expect(search_index_attribute_lookup['article'][0]['attachment'][1]['_name']).to eq 'some_file.txt'
  1904. end
  1905. end
  1906. context 'when es_attachment_max_size_in_mb takes only one attachment' do
  1907. before { Setting.set('es_attachment_max_size_in_mb', 4) }
  1908. it 'verify count of articles' do
  1909. expect(search_index_attribute_lookup['article'].count).to eq 3
  1910. end
  1911. it 'verify count of attachments' do
  1912. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 1
  1913. end
  1914. it 'verify if pdf exists' do
  1915. expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
  1916. end
  1917. end
  1918. context 'when es_attachment_max_size_in_mb takes no attachment' do
  1919. before { Setting.set('es_attachment_max_size_in_mb', 2) }
  1920. it 'verify count of articles' do
  1921. expect(search_index_attribute_lookup['article'].count).to eq 3
  1922. end
  1923. it 'verify count of attachments' do
  1924. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1925. end
  1926. end
  1927. context 'when es_total_max_size_in_mb takes no attachment and no oversized article' do
  1928. before { Setting.set('es_total_max_size_in_mb', 1) }
  1929. it 'verify count of articles' do
  1930. expect(search_index_attribute_lookup['article'].count).to eq 2
  1931. end
  1932. it 'verify count of attachments' do
  1933. expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
  1934. end
  1935. end
  1936. end
  1937. describe '#reopen_after_certain_time?' do
  1938. context 'when groups.follow_up_possible is set to "new_ticket_after_certain_time"' do
  1939. let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: 2) }
  1940. context 'when ticket is open' do
  1941. let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'open')) }
  1942. it 'returns false' do
  1943. expect(ticket.reopen_after_certain_time?).to be false
  1944. end
  1945. end
  1946. context 'when ticket is closed' do
  1947. let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'closed')) }
  1948. context 'when it is within configured time frame' do
  1949. it 'returns true' do
  1950. expect(ticket.reopen_after_certain_time?).to be true
  1951. end
  1952. end
  1953. context 'when it is outside configured time frame' do
  1954. before do
  1955. ticket
  1956. travel 3.days
  1957. end
  1958. it 'returns false' do
  1959. expect(ticket.reopen_after_certain_time?).to be false
  1960. end
  1961. end
  1962. end
  1963. context 'when reopen_time_in_days is not set' do
  1964. let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: -1) }
  1965. it 'returns false' do
  1966. expect(ticket.reopen_after_certain_time?).to be false
  1967. end
  1968. end
  1969. end
  1970. end
  1971. describe 'Automatic assignment assigns tickets in each group, not just the marked ones #4308' do
  1972. let(:ticket) { create(:ticket, group: Group.first, state: Ticket::State.find_by(name: 'closed')) }
  1973. let(:agent) { create(:agent, groups: [Group.first]) }
  1974. context 'when the condition does match' do
  1975. before do
  1976. Setting.set('ticket_auto_assignment', true)
  1977. Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.all.pluck(:id) } } })
  1978. end
  1979. it 'does auto assign' do
  1980. ticket.auto_assign(agent)
  1981. expect(ticket.reload.owner_id).to eq(agent.id)
  1982. end
  1983. end
  1984. context 'when the condition does not match' do
  1985. before do
  1986. Setting.set('ticket_auto_assignment', true)
  1987. Setting.set('ticket_auto_assignment_selector', { condition: { 'ticket.state_id' => { operator: 'is', value: Ticket::State.by_category(:work_on).pluck(:id) } } })
  1988. end
  1989. it 'does not auto assign' do
  1990. ticket.auto_assign(agent)
  1991. expect(ticket.reload.owner_id).to eq(1)
  1992. end
  1993. end
  1994. end
  1995. end