trigger_spec.rb 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  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/has_xss_sanitized_note_examples'
  5. RSpec.describe Trigger, type: :model do
  6. subject(:trigger) { create(:trigger, condition: condition, perform: perform) }
  7. it_behaves_like 'ApplicationModel', can_assets: { selectors: %i[condition perform] }
  8. it_behaves_like 'HasXssSanitizedNote', model_factory: :trigger
  9. describe 'validation' do
  10. it 'uses Validations::VerifyPerformRulesValidator' do
  11. expect(described_class).to have_validator(Validations::VerifyPerformRulesValidator).on(:perform)
  12. end
  13. end
  14. describe 'Send-email triggers' do
  15. before do
  16. described_class.destroy_all # Default DB state includes three sample triggers
  17. trigger # create subject trigger
  18. end
  19. let(:perform) do
  20. {
  21. 'notification.email' => {
  22. 'recipient' => 'ticket_customer',
  23. 'subject' => 'foo',
  24. 'body' => 'some body with >snip<#{article.body_as_html}>/snip<', # rubocop:disable Lint/InterpolationCheck
  25. }
  26. }
  27. end
  28. shared_examples 'include ticket attachment' do
  29. context 'notification.email include_attachments' do
  30. let(:perform) do
  31. {
  32. 'notification.email' => {
  33. 'recipient' => 'ticket_customer',
  34. 'subject' => 'Example subject',
  35. 'body' => 'Example body',
  36. }
  37. }.deep_merge(additional_options).deep_stringify_keys
  38. end
  39. let(:ticket) { create(:ticket) }
  40. shared_examples 'add a new article' do
  41. it 'adds a new article' do
  42. expect { TransactionDispatcher.commit }
  43. .to change(ticket.articles, :count).by(1)
  44. end
  45. end
  46. shared_examples 'add attachment to new article' do
  47. include_examples 'add a new article'
  48. it 'adds attachment to the new article' do
  49. ticket && trigger
  50. TransactionDispatcher.commit
  51. article = ticket.articles.last
  52. expect(article.type.name).to eq('email')
  53. expect(article.sender.name).to eq('System')
  54. expect(article.attachments.count).to eq(1)
  55. expect(article.attachments[0].filename).to eq('some_file.pdf')
  56. expect(article.attachments[0].preferences['Content-ID']).to eq('image/pdf@01CAB192.K8H512Y9')
  57. end
  58. end
  59. shared_examples 'does not add attachment to new article' do
  60. include_examples 'add a new article'
  61. it 'does not add attachment to the new article' do
  62. ticket && trigger
  63. TransactionDispatcher.commit
  64. article = ticket.articles.last
  65. expect(article.type.name).to eq('email')
  66. expect(article.sender.name).to eq('System')
  67. expect(article.attachments.count).to eq(0)
  68. end
  69. end
  70. context 'with include attachment present' do
  71. let(:additional_options) do
  72. {
  73. 'notification.email' => {
  74. include_attachments: 'true'
  75. }
  76. }
  77. end
  78. context 'when ticket has an attachment' do
  79. before do
  80. UserInfo.current_user_id = 1
  81. ticket_article = create(:ticket_article, ticket: ticket)
  82. create(:store,
  83. object: 'Ticket::Article',
  84. o_id: ticket_article.id,
  85. data: 'dGVzdCAxMjM=',
  86. filename: 'some_file.pdf',
  87. preferences: {
  88. 'Content-Type': 'image/pdf',
  89. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  90. })
  91. end
  92. include_examples 'add attachment to new article'
  93. end
  94. context 'when ticket does not have an attachment' do
  95. include_examples 'does not add attachment to new article'
  96. end
  97. end
  98. context 'with include attachment not present' do
  99. let(:additional_options) do
  100. {
  101. 'notification.email' => {
  102. include_attachments: 'false'
  103. }
  104. }
  105. end
  106. context 'when ticket has an attachment' do
  107. before do
  108. UserInfo.current_user_id = 1
  109. ticket_article = create(:ticket_article, ticket: ticket)
  110. create(:store,
  111. object: 'Ticket::Article',
  112. o_id: ticket_article.id,
  113. data: 'dGVzdCAxMjM=',
  114. filename: 'some_file.pdf',
  115. preferences: {
  116. 'Content-Type': 'image/pdf',
  117. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  118. })
  119. end
  120. include_examples 'does not add attachment to new article'
  121. end
  122. context 'when ticket does not have an attachment' do
  123. include_examples 'does not add attachment to new article'
  124. end
  125. end
  126. end
  127. end
  128. context 'for condition "ticket created"' do
  129. let(:condition) do
  130. { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } }
  131. end
  132. context 'when ticket is created directly' do
  133. let!(:ticket) { create(:ticket) }
  134. it 'fires (without altering ticket state)' do
  135. expect { TransactionDispatcher.commit }
  136. .to change(Ticket::Article, :count).by(1)
  137. .and not_change { ticket.reload.state.name }.from('new')
  138. end
  139. end
  140. context 'when ticket has tags' do
  141. let(:tag1) { create(:'tag/item', name: 't1') }
  142. let(:tag2) { create(:'tag/item', name: 't2') }
  143. let(:tag3) { create(:'tag/item', name: 't3') }
  144. let!(:ticket) do
  145. ticket = create(:ticket)
  146. create(:tag, o: ticket, tag_item: tag1)
  147. create(:tag, o: ticket, tag_item: tag2)
  148. create(:tag, o: ticket, tag_item: tag3)
  149. ticket
  150. end
  151. let(:perform) do
  152. {
  153. 'notification.email' => {
  154. 'recipient' => 'ticket_customer',
  155. 'subject' => 'foo',
  156. 'body' => 'some body with #{ticket.tags}', # rubocop:disable Lint/InterpolationCheck
  157. }
  158. }
  159. end
  160. it 'fires body with replaced tags' do
  161. TransactionDispatcher.commit
  162. expect(Ticket::Article.last.body).to eq('some body with t1, t2, t3')
  163. end
  164. end
  165. context 'when ticket is created via Channel::EmailParser.process' do
  166. before { create(:email_address, groups: [Group.first]) }
  167. let(:raw_email) { Rails.root.join('test/data/mail/mail001.box').read }
  168. it 'fires (without altering ticket state)' do
  169. expect { Channel::EmailParser.new.process({}, raw_email) }
  170. .to change(Ticket, :count).by(1)
  171. .and change(Ticket::Article, :count).by(2)
  172. expect(Ticket.last.state.name).to eq('new')
  173. end
  174. end
  175. context 'when ticket is created via Channel::EmailParser.process with inline image' do
  176. before { create(:email_address, groups: [Group.first]) }
  177. let(:raw_email) { Rails.root.join('test/data/mail/mail010.box').read }
  178. it 'fires (without altering ticket state)' do
  179. expect { Channel::EmailParser.new.process({}, raw_email) }
  180. .to change(Ticket, :count).by(1)
  181. .and change(Ticket::Article, :count).by(2)
  182. expect(Ticket.last.state.name).to eq('new')
  183. article = Ticket::Article.last
  184. expect(article.type.name).to eq('email')
  185. expect(article.sender.name).to eq('System')
  186. expect(article.attachments.count).to eq(1)
  187. expect(article.attachments[0].filename).to eq('image001.jpg')
  188. expect(article.attachments[0].preferences['Content-ID']).to eq('image001.jpg@01CDB132.D8A510F0')
  189. expect(article.body).to eq(<<~RAW.chomp
  190. some body with &gt;snip&lt;<div>
  191. <p>Herzliche Grüße aus Oberalteich sendet Herrn Smith</p>
  192. <p> </p>
  193. <p>Sepp Smith - Dipl.Ing. agr. (FH)</p>
  194. <p>Geschäftsführer der example Straubing-Bogen</p>
  195. <p>Klosterhof 1 | 94327 Bogen-Oberalteich</p>
  196. <p>Tel: 09422-505601 | Fax: 09422-505620</p>
  197. <p><span>Internet: <a href="http://example-straubing-bogen.de/" rel="nofollow noreferrer noopener" target="_blank"><span style="color:blue;">http://example-straubing-bogen.de</span></a></span></p>
  198. <p><span lang="EN-US">Facebook: </span><a href="http://facebook.de/examplesrbog" rel="nofollow noreferrer noopener" target="_blank"><span lang="EN-US" style="color:blue;">http://facebook.de/examplesrbog</span></a><span lang="EN-US"></span></p>
  199. <p><b><span style="color:navy;"><img border="0" src="cid:image001.jpg@01CDB132.D8A510F0" alt="Beschreibung: Beschreibung: efqmLogo" style="width:60px;height:19px;"></span></b><b><span lang="EN-US" style="color:navy;"> - European Foundation für Quality Management</span></b><span lang="EN-US"></span></p>
  200. <p><span lang="EN-US"> </span></p>
  201. </div>&gt;/snip&lt;
  202. RAW
  203. )
  204. end
  205. end
  206. context 'notification.email recipient' do
  207. let!(:ticket) { create(:ticket) }
  208. let!(:recipient1) { create(:user, email: 'test1@zammad-test.com') }
  209. let!(:recipient2) { create(:user, email: 'test2@zammad-test.com') }
  210. let!(:recipient3) { create(:user, email: 'test3@zammad-test.com') }
  211. let(:perform) do
  212. {
  213. 'notification.email' => {
  214. 'recipient' => recipient,
  215. 'subject' => 'Hello',
  216. 'body' => 'World!'
  217. }
  218. }
  219. end
  220. before { TransactionDispatcher.commit }
  221. context 'mix of recipient group keyword and single recipient users' do
  222. let(:recipient) { [ 'ticket_customer', "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] }
  223. it 'contains all recipients' do
  224. expect(ticket.articles.last.to).to eq("#{ticket.customer.email}, #{recipient1.email}, #{recipient2.email}, #{recipient3.email}")
  225. end
  226. context 'duplicate recipient' do
  227. let(:recipient) { [ 'ticket_customer', "userid_#{ticket.customer.id}" ] }
  228. it 'contains only one recipient' do
  229. expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s)
  230. end
  231. end
  232. end
  233. context 'list of single users only' do
  234. let(:recipient) { [ "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] }
  235. it 'contains all recipients' do
  236. expect(ticket.articles.last.to).to eq("#{recipient1.email}, #{recipient2.email}, #{recipient3.email}")
  237. end
  238. context 'assets' do
  239. it 'resolves Users from recipient list' do
  240. expect(trigger.assets({})[:User].keys).to include(recipient1.id, recipient2.id, recipient3.id)
  241. end
  242. context 'single entry' do
  243. let(:recipient) { "userid_#{recipient1.id}" }
  244. it 'resolves User from recipient list' do
  245. expect(trigger.assets({})[:User].keys).to include(recipient1.id)
  246. end
  247. end
  248. end
  249. end
  250. context 'recipient group keyword only' do
  251. let(:recipient) { 'ticket_customer' }
  252. it 'contains matching recipient' do
  253. expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s)
  254. end
  255. end
  256. end
  257. context 'active S/MIME integration' do
  258. before do
  259. Setting.set('smime_integration', true)
  260. create(:smime_certificate, :with_private, fixture: system_email_address)
  261. create(:smime_certificate, fixture: customer_email_address)
  262. end
  263. let(:system_email_address) { 'smime1@example.com' }
  264. let(:customer_email_address) { 'smime2@example.com' }
  265. let(:email_address) { create(:email_address, email: system_email_address) }
  266. let(:group) { create(:group, email_address: email_address) }
  267. let(:customer) { create(:customer, email: customer_email_address) }
  268. let(:security_preferences) { Ticket::Article.last.preferences[:security] }
  269. let(:perform) do
  270. {
  271. 'notification.email' => {
  272. 'recipient' => 'ticket_customer',
  273. 'subject' => 'Subject dummy.',
  274. 'body' => 'Body dummy.',
  275. }.merge(security_configuration)
  276. }
  277. end
  278. let!(:ticket) { create(:ticket, group: group, customer: customer) }
  279. context 'sending articles' do
  280. before do
  281. TransactionDispatcher.commit
  282. end
  283. context 'expired certificate' do
  284. let(:system_email_address) { 'expiredsmime1@example.com' }
  285. let(:security_configuration) do
  286. {
  287. 'sign' => 'always',
  288. 'encryption' => 'always',
  289. }
  290. end
  291. it 'creates unsigned article' do
  292. expect(security_preferences[:sign][:success]).to be false
  293. expect(security_preferences[:encryption][:success]).to be true
  294. end
  295. end
  296. context 'sign and encryption not set' do
  297. let(:security_configuration) { {} }
  298. it 'does not sign or encrypt' do
  299. expect(security_preferences[:sign][:success]).to be false
  300. expect(security_preferences[:encryption][:success]).to be false
  301. end
  302. end
  303. context 'sign and encryption disabled' do
  304. let(:security_configuration) do
  305. {
  306. 'sign' => 'no',
  307. 'encryption' => 'no',
  308. }
  309. end
  310. it 'does not sign or encrypt' do
  311. expect(security_preferences[:sign][:success]).to be false
  312. expect(security_preferences[:encryption][:success]).to be false
  313. end
  314. end
  315. context 'sign is enabled' do
  316. let(:security_configuration) do
  317. {
  318. 'sign' => 'always',
  319. 'encryption' => 'no',
  320. }
  321. end
  322. it 'signs' do
  323. expect(security_preferences[:sign][:success]).to be true
  324. expect(security_preferences[:encryption][:success]).to be false
  325. end
  326. end
  327. context 'encryption enabled' do
  328. let(:security_configuration) do
  329. {
  330. 'sign' => 'no',
  331. 'encryption' => 'always',
  332. }
  333. end
  334. it 'encrypts' do
  335. expect(security_preferences[:sign][:success]).to be false
  336. expect(security_preferences[:encryption][:success]).to be true
  337. end
  338. end
  339. context 'sign and encryption enabled' do
  340. let(:security_configuration) do
  341. {
  342. 'sign' => 'always',
  343. 'encryption' => 'always',
  344. }
  345. end
  346. it 'signs and encrypts' do
  347. expect(security_preferences[:sign][:success]).to be true
  348. expect(security_preferences[:encryption][:success]).to be true
  349. end
  350. end
  351. end
  352. context 'discard' do
  353. context 'sign' do
  354. let(:security_configuration) do
  355. {
  356. 'sign' => 'discard',
  357. }
  358. end
  359. context 'group without certificate' do
  360. let(:group) { create(:group) }
  361. it 'does not fire' do
  362. expect { TransactionDispatcher.commit }
  363. .not_to change(Ticket::Article, :count)
  364. end
  365. end
  366. end
  367. context 'encryption' do
  368. let(:security_configuration) do
  369. {
  370. 'encryption' => 'discard',
  371. }
  372. end
  373. context 'customer without certificate' do
  374. let(:customer) { create(:customer) }
  375. it 'does not fire' do
  376. expect { TransactionDispatcher.commit }
  377. .not_to change(Ticket::Article, :count)
  378. end
  379. end
  380. end
  381. context 'mixed' do
  382. context 'sign' do
  383. let(:security_configuration) do
  384. {
  385. 'encryption' => 'always',
  386. 'sign' => 'discard',
  387. }
  388. end
  389. context 'group without certificate' do
  390. let(:group) { create(:group) }
  391. it 'does not fire' do
  392. expect { TransactionDispatcher.commit }
  393. .not_to change(Ticket::Article, :count)
  394. end
  395. end
  396. end
  397. context 'encryption' do
  398. let(:security_configuration) do
  399. {
  400. 'encryption' => 'discard',
  401. 'sign' => 'always',
  402. }
  403. end
  404. context 'customer without certificate' do
  405. let(:customer) { create(:customer) }
  406. it 'does not fire' do
  407. expect { TransactionDispatcher.commit }
  408. .not_to change(Ticket::Article, :count)
  409. end
  410. end
  411. end
  412. end
  413. end
  414. end
  415. include_examples 'include ticket attachment'
  416. end
  417. context 'for condition "ticket updated"' do
  418. let(:condition) do
  419. { 'ticket.action' => { 'operator' => 'is', 'value' => 'update' } }
  420. end
  421. let!(:ticket) { create(:ticket).tap { TransactionDispatcher.commit } }
  422. context 'when new article is created directly' do
  423. context 'with empty #preferences hash' do
  424. let!(:article) { create(:ticket_article, ticket: ticket) }
  425. it 'fires (without altering ticket state)' do
  426. expect { TransactionDispatcher.commit }
  427. .to change { ticket.reload.articles.count }.by(1)
  428. .and not_change { ticket.reload.state.name }.from('new')
  429. end
  430. end
  431. context 'with #preferences { "send-auto-response" => false }' do
  432. let!(:article) do
  433. create(:ticket_article,
  434. ticket: ticket,
  435. preferences: { 'send-auto-response' => false })
  436. end
  437. it 'does not fire' do
  438. expect { TransactionDispatcher.commit }
  439. .not_to change { ticket.reload.articles.count }
  440. end
  441. end
  442. end
  443. context 'when new article is created via Channel::EmailParser.process' do
  444. context 'with a regular message' do
  445. let!(:article) do
  446. create(:ticket_article,
  447. ticket: ticket,
  448. message_id: raw_email[%r{(?<=^References: )\S*}],
  449. subject: raw_email[%r{(?<=^Subject: Re: ).*$}])
  450. end
  451. let(:raw_email) { Rails.root.join('test/data/mail/mail005.box').read }
  452. it 'fires (without altering ticket state)' do
  453. expect { Channel::EmailParser.new.process({}, raw_email) }
  454. .to not_change { Ticket.count }
  455. .and change { ticket.reload.articles.count }.by(2)
  456. .and not_change { ticket.reload.state.name }.from('new')
  457. end
  458. end
  459. context 'with delivery-failed "bounce message"' do
  460. let!(:article) do
  461. create(:ticket_article,
  462. ticket: ticket,
  463. message_id: raw_email[%r{(?<=^Message-ID: )\S*}])
  464. end
  465. let(:raw_email) { Rails.root.join('test/data/mail/mail055.box').read }
  466. it 'does not fire' do
  467. expect { Channel::EmailParser.new.process({}, raw_email) }
  468. .to change { ticket.reload.articles.count }.by(1)
  469. end
  470. end
  471. end
  472. # https://github.com/zammad/zammad/issues/3991
  473. context 'when article contains a mention' do
  474. let!(:article) do
  475. create(:ticket_article,
  476. ticket: ticket,
  477. body: '<a href="http:/#user/profile/1" data-mention-user-id="1" rel="nofollow noreferrer noopener" target="_blank" title="http:/#user/profile/1">Test Admin Agent</a> test<br>')
  478. end
  479. it 'fires correctly' do
  480. expect { TransactionDispatcher.commit }
  481. .to change { ticket.reload.articles.count }.by(1)
  482. end
  483. end
  484. end
  485. context 'with condition execution_time.calendar_id' do
  486. let(:calendar) { create(:calendar) }
  487. let(:perform) do
  488. { 'ticket.title'=>{ 'value'=>'triggered' } }
  489. end
  490. let!(:ticket) { create(:ticket, title: 'Test Ticket') }
  491. context 'is in working time' do
  492. let(:condition) do
  493. { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) }, 'execution_time.calendar_id' => { 'operator' => 'is in working time', 'value' => calendar.id } }
  494. end
  495. it 'does trigger only in working time' do
  496. travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
  497. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  498. end
  499. it 'does not trigger out of working time' do
  500. travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
  501. TransactionDispatcher.commit
  502. expect(ticket.reload.title).to eq('Test Ticket')
  503. end
  504. end
  505. context 'is not in working time' do
  506. let(:condition) do
  507. { 'execution_time.calendar_id' => { 'operator' => 'is not in working time', 'value' => calendar.id } }
  508. end
  509. it 'does not trigger in working time' do
  510. travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
  511. TransactionDispatcher.commit
  512. expect(ticket.reload.title).to eq('Test Ticket')
  513. end
  514. it 'does trigger out of working time' do
  515. travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
  516. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  517. end
  518. end
  519. end
  520. context 'with article last sender equals system address' do
  521. let!(:ticket) { create(:ticket) }
  522. let(:perform) do
  523. {
  524. 'notification.email' => {
  525. 'recipient' => 'article_last_sender',
  526. 'subject' => 'foo last sender',
  527. 'body' => 'some body with &gt;snip&lt;#{article.body_as_html}&gt;/snip&lt;', # rubocop:disable Lint/InterpolationCheck
  528. }
  529. }
  530. end
  531. let(:condition) do
  532. { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) } }
  533. end
  534. let!(:system_address) do
  535. create(:email_address)
  536. end
  537. context 'article with from equal to the a system address' do
  538. let!(:article) do
  539. create(:ticket_article,
  540. ticket: ticket,
  541. from: system_address.email,)
  542. end
  543. it 'does not trigger because of the last article is created my system address' do
  544. expect { TransactionDispatcher.commit }.not_to change { ticket.reload.articles.count }
  545. expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender')
  546. expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email)
  547. end
  548. end
  549. context 'article with reply_to equal to the a system address' do
  550. let!(:article) do
  551. create(:ticket_article,
  552. ticket: ticket,
  553. from: system_address.email,
  554. reply_to: system_address.email,)
  555. end
  556. it 'does not trigger because of the last article is created my system address' do
  557. expect { TransactionDispatcher.commit }.not_to change { ticket.reload.articles.count }
  558. expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender')
  559. expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email)
  560. end
  561. end
  562. include_examples 'include ticket attachment'
  563. end
  564. end
  565. context 'with pre condition current_user.id' do
  566. let(:perform) do
  567. { 'ticket.title'=>{ 'value'=>'triggered' } }
  568. end
  569. let(:user) do
  570. user = create(:agent)
  571. user.roles.first.groups << group
  572. user
  573. end
  574. let(:group) { Group.first }
  575. let(:ticket) do
  576. create(:ticket,
  577. title: 'Test Ticket', group: group,
  578. owner_id: user.id, created_by_id: user.id, updated_by_id: user.id)
  579. end
  580. shared_examples 'successful trigger' do |attribute:|
  581. let(:attribute) { attribute }
  582. let(:condition) do
  583. { attribute => { operator: 'is', pre_condition: 'current_user.id', value: '', value_completion: '' } }
  584. end
  585. it "for #{attribute}" do
  586. ticket && trigger
  587. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  588. end
  589. end
  590. it_behaves_like 'successful trigger', attribute: 'ticket.updated_by_id'
  591. it_behaves_like 'successful trigger', attribute: 'ticket.owner_id'
  592. end
  593. describe 'Multi-trigger interactions:' do
  594. let(:ticket) { create(:ticket) }
  595. context 'cascading (i.e., trigger A satisfies trigger B satisfies trigger C)' do
  596. subject!(:triggers) do
  597. [
  598. create(:trigger, condition: initial_state, perform: first_change, name: 'A'),
  599. create(:trigger, condition: first_change, perform: second_change, name: 'B'),
  600. create(:trigger, condition: second_change, perform: third_change, name: 'C')
  601. ]
  602. end
  603. context 'in a chain' do
  604. let(:initial_state) do
  605. {
  606. 'ticket.state_id' => {
  607. 'operator' => 'is',
  608. 'value' => Ticket::State.lookup(name: 'new').id.to_s,
  609. }
  610. }
  611. end
  612. let(:first_change) do
  613. {
  614. 'ticket.state_id' => {
  615. 'operator' => 'is',
  616. 'value' => Ticket::State.lookup(name: 'open').id.to_s,
  617. }
  618. }
  619. end
  620. let(:second_change) do
  621. {
  622. 'ticket.state_id' => {
  623. 'operator' => 'is',
  624. 'value' => Ticket::State.lookup(name: 'closed').id.to_s,
  625. }
  626. }
  627. end
  628. let(:third_change) do
  629. {
  630. 'ticket.state_id' => {
  631. 'operator' => 'is',
  632. 'value' => Ticket::State.lookup(name: 'merged').id.to_s,
  633. }
  634. }
  635. end
  636. context 'in alphabetical order (by name)' do
  637. it 'fires all triggers in sequence' do
  638. expect { TransactionDispatcher.commit }
  639. .to change { ticket.reload.state.name }.to('merged')
  640. end
  641. end
  642. context 'out of alphabetical order (by name)' do
  643. before do
  644. triggers.first.update(name: 'E')
  645. triggers.second.update(name: 'F')
  646. triggers.third.update(name: 'D')
  647. end
  648. context 'with Setting ticket_trigger_recursive: true' do
  649. before { Setting.set('ticket_trigger_recursive', true) }
  650. it 'evaluates triggers in sequence, then loops back to the start and re-evalutes skipped triggers' do
  651. expect { TransactionDispatcher.commit }
  652. .to change { ticket.reload.state.name }.to('merged')
  653. end
  654. end
  655. context 'with Setting ticket_trigger_recursive: false' do
  656. before { Setting.set('ticket_trigger_recursive', false) }
  657. it 'evaluates triggers in sequence, firing only the ones that match' do
  658. expect { TransactionDispatcher.commit }
  659. .to change { ticket.reload.state.name }.to('closed')
  660. end
  661. end
  662. end
  663. end
  664. context 'in circular reference (i.e., trigger A satisfies trigger B satisfies trigger C satisfies trigger A...)' do
  665. let(:initial_state) do
  666. {
  667. 'ticket.priority_id' => {
  668. 'operator' => 'is',
  669. 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s,
  670. }
  671. }
  672. end
  673. let(:first_change) do
  674. {
  675. 'ticket.priority_id' => {
  676. 'operator' => 'is',
  677. 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s,
  678. }
  679. }
  680. end
  681. let(:second_change) do
  682. {
  683. 'ticket.priority_id' => {
  684. 'operator' => 'is',
  685. 'value' => Ticket::Priority.lookup(name: '1 low').id.to_s,
  686. }
  687. }
  688. end
  689. let(:third_change) do
  690. {
  691. 'ticket.priority_id' => {
  692. 'operator' => 'is',
  693. 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s,
  694. }
  695. }
  696. end
  697. context 'with Setting ticket_trigger_recursive: true' do
  698. before { Setting.set('ticket_trigger_recursive', true) }
  699. it 'fires each trigger once, without being caught in an endless loop' do
  700. expect { Timeout.timeout(2) { TransactionDispatcher.commit } }
  701. .to not_change { ticket.reload.priority.name }
  702. .and not_raise_error
  703. end
  704. end
  705. context 'with Setting ticket_trigger_recursive: false' do
  706. before { Setting.set('ticket_trigger_recursive', false) }
  707. it 'fires each trigger once, without being caught in an endless loop' do
  708. expect { Timeout.timeout(2) { TransactionDispatcher.commit } }
  709. .to not_change { ticket.reload.priority.name }
  710. .and not_raise_error
  711. end
  712. end
  713. end
  714. end
  715. context 'competing (i.e., trigger A un-satisfies trigger B)' do
  716. subject!(:triggers) do
  717. [
  718. create(:trigger, condition: initial_state, perform: change_a, name: 'A'),
  719. create(:trigger, condition: initial_state, perform: change_b, name: 'B')
  720. ]
  721. end
  722. let(:initial_state) do
  723. {
  724. 'ticket.state_id' => {
  725. 'operator' => 'is',
  726. 'value' => Ticket::State.lookup(name: 'new').id.to_s,
  727. }
  728. }
  729. end
  730. let(:change_a) do
  731. {
  732. 'ticket.state_id' => {
  733. 'operator' => 'is',
  734. 'value' => Ticket::State.lookup(name: 'open').id.to_s,
  735. }
  736. }
  737. end
  738. let(:change_b) do
  739. {
  740. 'ticket.priority_id' => {
  741. 'operator' => 'is',
  742. 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s,
  743. }
  744. }
  745. end
  746. it 'evaluates triggers in sequence, firing only the ones that match' do
  747. expect { TransactionDispatcher.commit }
  748. .to change { ticket.reload.state.name }.to('open')
  749. .and not_change { ticket.reload.priority.name }
  750. end
  751. end
  752. end
  753. describe 'multiselect triggers', db_strategy: :reset, mariadb: true do
  754. let(:attribute_name) { 'multiselect' }
  755. let(:condition) do
  756. { "ticket.#{attribute_name}" => { 'operator' => operator, 'value' => trigger_values } }
  757. end
  758. let(:perform) do
  759. { 'article.note' => { 'subject' => 'Test subject note', 'internal' => 'true', 'body' => 'Test body note' } }
  760. end
  761. before do
  762. create(:object_manager_attribute_multiselect, name: attribute_name)
  763. ObjectManager::Attribute.migration_execute
  764. described_class.destroy_all # Default DB state includes three sample triggers
  765. trigger # create subject trigger
  766. end
  767. context 'when ticket is updated with a multiselect trigger condition', authenticated_as: :owner, db_strategy: :reset do
  768. let(:options) do
  769. {
  770. a: 'a',
  771. b: 'b',
  772. c: 'c',
  773. d: 'd',
  774. e: 'e',
  775. }
  776. end
  777. let(:trigger_values) { %w[a b c] }
  778. let(:group) { create(:group) }
  779. let(:owner) { create(:admin, group_ids: [group.id]) }
  780. let!(:ticket) { create(:ticket, group: group,) }
  781. before do
  782. ticket.update_attribute(attribute_name, ticket_multiselect_values)
  783. end
  784. shared_examples 'updating the ticket with the trigger condition' do
  785. it 'updates the ticket with the trigger condition' do
  786. expect { TransactionDispatcher.commit }
  787. .to change(Ticket::Article, :count).by(1)
  788. end
  789. end
  790. shared_examples 'not updating the ticket with the trigger condition' do
  791. it 'does not update the ticket with the trigger condition' do
  792. expect { TransactionDispatcher.commit }
  793. .to not_change(Ticket::Article, :count)
  794. end
  795. end
  796. context "with 'contains all' used" do
  797. let(:operator) { 'contains all' }
  798. context 'when updated value is the same with trigger value' do
  799. let(:ticket_multiselect_values) { trigger_values }
  800. it_behaves_like 'updating the ticket with the trigger condition'
  801. end
  802. context 'when updated value is different from the trigger value' do
  803. let(:ticket_multiselect_values) { options.values - trigger_values }
  804. it_behaves_like 'not updating the ticket with the trigger condition'
  805. end
  806. context 'when no value is selected' do
  807. let(:ticket_multiselect_values) { ['-'] }
  808. it_behaves_like 'not updating the ticket with the trigger condition'
  809. end
  810. context 'when all value is selected' do
  811. let(:ticket_multiselect_values) { options.values }
  812. it_behaves_like 'updating the ticket with the trigger condition'
  813. end
  814. context 'when updated value contains one of the trigger value' do
  815. let(:ticket_multiselect_values) { [trigger_values.first] }
  816. it_behaves_like 'not updating the ticket with the trigger condition'
  817. end
  818. context 'when updated value does not contain one of the trigger value' do
  819. let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
  820. it_behaves_like 'not updating the ticket with the trigger condition'
  821. end
  822. end
  823. context "with 'contains one' used" do
  824. let(:operator) { 'contains one' }
  825. context 'when updated value is the same with trigger value' do
  826. let(:ticket_multiselect_values) { trigger_values }
  827. it_behaves_like 'updating the ticket with the trigger condition'
  828. end
  829. context 'when updated value is different from the trigger value' do
  830. let(:ticket_multiselect_values) { options.values - trigger_values }
  831. it_behaves_like 'not updating the ticket with the trigger condition'
  832. end
  833. context 'when no value is selected' do
  834. let(:ticket_multiselect_values) { ['-'] }
  835. it_behaves_like 'not updating the ticket with the trigger condition'
  836. end
  837. context 'when all value is selected' do
  838. let(:ticket_multiselect_values) { options.values }
  839. it_behaves_like 'updating the ticket with the trigger condition'
  840. end
  841. context 'when updated value contains only one of the trigger value' do
  842. let(:ticket_multiselect_values) { [trigger_values.first] }
  843. it_behaves_like 'updating the ticket with the trigger condition'
  844. end
  845. context 'when updated value does not contain one of the trigger value' do
  846. let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
  847. it_behaves_like 'updating the ticket with the trigger condition'
  848. end
  849. end
  850. context "with 'contains all not' used" do
  851. let(:operator) { 'contains all not' }
  852. context 'when updated value is the same with trigger value' do
  853. let(:ticket_multiselect_values) { trigger_values }
  854. it_behaves_like 'not updating the ticket with the trigger condition'
  855. end
  856. context 'when updated value is different from the trigger value' do
  857. let(:ticket_multiselect_values) { options.values - trigger_values }
  858. it_behaves_like 'updating the ticket with the trigger condition'
  859. end
  860. context 'when no value is selected' do
  861. let(:ticket_multiselect_values) { ['-'] }
  862. it_behaves_like 'updating the ticket with the trigger condition'
  863. end
  864. context 'when all value is selected' do
  865. let(:ticket_multiselect_values) { options.values }
  866. it_behaves_like 'not updating the ticket with the trigger condition'
  867. end
  868. context 'when updated value contains only one of the trigger value' do
  869. let(:ticket_multiselect_values) { [trigger_values.first] }
  870. it_behaves_like 'updating the ticket with the trigger condition'
  871. end
  872. context 'when updated value does not contain one of the trigger value' do
  873. let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
  874. it_behaves_like 'updating the ticket with the trigger condition'
  875. end
  876. end
  877. context "with 'contains one not' used" do
  878. let(:operator) { 'contains one not' }
  879. context 'when updated value is the same with trigger value' do
  880. let(:ticket_multiselect_values) { trigger_values }
  881. it_behaves_like 'not updating the ticket with the trigger condition'
  882. end
  883. context 'when updated value is different from the trigger value' do
  884. let(:ticket_multiselect_values) { options.values - trigger_values }
  885. it_behaves_like 'updating the ticket with the trigger condition'
  886. end
  887. context 'when no value is selected' do
  888. let(:ticket_multiselect_values) { ['-'] }
  889. it_behaves_like 'updating the ticket with the trigger condition'
  890. end
  891. context 'when all value is selected' do
  892. let(:ticket_multiselect_values) { options.values }
  893. it_behaves_like 'not updating the ticket with the trigger condition'
  894. end
  895. context 'when updated value contains only one of the trigger value' do
  896. let(:ticket_multiselect_values) { [trigger_values.first] }
  897. it_behaves_like 'not updating the ticket with the trigger condition'
  898. end
  899. context 'when updated value does not contain one of the trigger value' do
  900. let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
  901. it_behaves_like 'not updating the ticket with the trigger condition'
  902. end
  903. end
  904. end
  905. end
  906. describe 'Triggers without configured action inside condition are executed differently compared to 5.3 #4550' do
  907. let(:ticket_match) { create(:ticket, group: Group.first) }
  908. let(:ticket_no_match) { create(:ticket, group: Group.first, priority: Ticket::Priority.find_by(name: '1 low')) }
  909. let(:condition) do
  910. { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.where(name: ['2 normal', '3 high']).pluck(:id).map(&:to_s) } }
  911. end
  912. let(:perform) do
  913. { 'article.note' => { 'subject' => 'Test subject note', 'internal' => 'true', 'body' => 'Test body note' } }
  914. end
  915. before do
  916. ticket_match
  917. ticket_no_match
  918. trigger
  919. TransactionDispatcher.commit
  920. end
  921. context 'when conditions match' do
  922. it 'does not create an article if the state changes' do
  923. ticket_match.update(state: Ticket::State.find_by(name: 'closed'))
  924. expect { TransactionDispatcher.commit }.not_to change(Ticket::Article, :count)
  925. end
  926. it 'does create an article if priority changes' do
  927. ticket_match.update(priority: Ticket::Priority.find_by(name: '3 high'))
  928. expect { TransactionDispatcher.commit }.to change(Ticket::Article, :count).by(1)
  929. end
  930. it 'does create an article if priority matches and new article is created' do
  931. create(:ticket_article, ticket: ticket_match)
  932. expect { TransactionDispatcher.commit }.to change(Ticket::Article, :count).by(1)
  933. end
  934. end
  935. context "when conditions don't match" do
  936. it 'does not create an article if priority does not match but new article is created' do
  937. create(:ticket_article, ticket: ticket_no_match)
  938. expect { TransactionDispatcher.commit }.not_to change(Ticket::Article, :count)
  939. end
  940. it 'does not create an article if priority does not match and priority changes to low' do
  941. ticket_match.update(priority: Ticket::Priority.find_by(name: '1 low'))
  942. expect { TransactionDispatcher.commit }.not_to change(Ticket::Article, :count)
  943. end
  944. end
  945. end
  946. end