trigger_spec.rb 38 KB

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