trigger_spec.rb 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. # Copyright (C) 2012-2021 Zammad Foundation, http://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. context 'for condition "ticket created"' do
  47. let(:condition) do
  48. { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } }
  49. end
  50. context 'when ticket is created directly' do
  51. let!(:ticket) { create(:ticket) }
  52. it 'fires (without altering ticket state)' do
  53. expect { TransactionDispatcher.commit }
  54. .to change(Ticket::Article, :count).by(1)
  55. .and not_change { ticket.reload.state.name }.from('new')
  56. end
  57. end
  58. context 'when ticket has tags' do
  59. let(:tag1) { create(:'tag/item', name: 't1') }
  60. let(:tag2) { create(:'tag/item', name: 't2') }
  61. let(:tag3) { create(:'tag/item', name: 't3') }
  62. let!(:ticket) do
  63. ticket = create(:ticket)
  64. create(:tag, o: ticket, tag_item: tag1)
  65. create(:tag, o: ticket, tag_item: tag2)
  66. create(:tag, o: ticket, tag_item: tag3)
  67. ticket
  68. end
  69. let(:perform) do
  70. {
  71. 'notification.email' => {
  72. 'recipient' => 'ticket_customer',
  73. 'subject' => 'foo',
  74. 'body' => 'some body with #{ticket.tags}', # rubocop:disable Lint/InterpolationCheck
  75. }
  76. }
  77. end
  78. it 'fires body with replaced tags' do
  79. TransactionDispatcher.commit
  80. expect(Ticket::Article.last.body).to eq('some body with t1, t2, t3')
  81. end
  82. end
  83. context 'when ticket is created via Channel::EmailParser.process' do
  84. before { create(:email_address, groups: [Group.first]) }
  85. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail001.box')) }
  86. it 'fires (without altering ticket state)' do
  87. expect { Channel::EmailParser.new.process({}, raw_email) }
  88. .to change(Ticket, :count).by(1)
  89. .and change { Ticket::Article.count }.by(2)
  90. expect(Ticket.last.state.name).to eq('new')
  91. end
  92. end
  93. context 'when ticket is created via Channel::EmailParser.process with inline image' do
  94. before { create(:email_address, groups: [Group.first]) }
  95. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail010.box')) }
  96. it 'fires (without altering ticket state)' do
  97. expect { Channel::EmailParser.new.process({}, raw_email) }
  98. .to change(Ticket, :count).by(1)
  99. .and change { Ticket::Article.count }.by(2)
  100. expect(Ticket.last.state.name).to eq('new')
  101. article = Ticket::Article.last
  102. expect(article.type.name).to eq('email')
  103. expect(article.sender.name).to eq('System')
  104. expect(article.attachments.count).to eq(1)
  105. expect(article.attachments[0].filename).to eq('image001.jpg')
  106. expect(article.attachments[0].preferences['Content-ID']).to eq('image001.jpg@01CDB132.D8A510F0')
  107. expect(article.body).to eq(<<~RAW.chomp
  108. some body with &gt;snip&lt;<div>
  109. <p>Herzliche Grüße aus Oberalteich sendet Herrn Smith</p>
  110. <p> </p>
  111. <p>Sepp Smith - Dipl.Ing. agr. (FH)</p>
  112. <p>Geschäftsführer der example Straubing-Bogen</p>
  113. <p>Klosterhof 1 | 94327 Bogen-Oberalteich</p>
  114. <p>Tel: 09422-505601 | Fax: 09422-505620</p>
  115. <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>
  116. <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>
  117. <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>
  118. <p><span lang="EN-US"><p> </p></span></p>
  119. </div>&gt;/snip&lt;
  120. RAW
  121. )
  122. end
  123. end
  124. context 'notification.email recipient' do
  125. let!(:ticket) { create(:ticket) }
  126. let!(:recipient1) { create(:user, email: 'test1@zammad-test.com') }
  127. let!(:recipient2) { create(:user, email: 'test2@zammad-test.com') }
  128. let!(:recipient3) { create(:user, email: 'test3@zammad-test.com') }
  129. let(:perform) do
  130. {
  131. 'notification.email' => {
  132. 'recipient' => recipient,
  133. 'subject' => 'Hello',
  134. 'body' => 'World!'
  135. }
  136. }
  137. end
  138. before { TransactionDispatcher.commit }
  139. context 'mix of recipient group keyword and single recipient users' do
  140. let(:recipient) { [ 'ticket_customer', "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] }
  141. it 'contains all recipients' do
  142. expect(ticket.articles.last.to).to eq("#{ticket.customer.email}, #{recipient1.email}, #{recipient2.email}, #{recipient3.email}")
  143. end
  144. context 'duplicate recipient' do
  145. let(:recipient) { [ 'ticket_customer', "userid_#{ticket.customer.id}" ] }
  146. it 'contains only one recipient' do
  147. expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s)
  148. end
  149. end
  150. end
  151. context 'list of single users only' do
  152. let(:recipient) { [ "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] }
  153. it 'contains all recipients' do
  154. expect(ticket.articles.last.to).to eq("#{recipient1.email}, #{recipient2.email}, #{recipient3.email}")
  155. end
  156. context 'assets' do
  157. it 'resolves Users from recipient list' do
  158. expect(trigger.assets({})[:User].keys).to include(recipient1.id, recipient2.id, recipient3.id)
  159. end
  160. context 'single entry' do
  161. let(:recipient) { "userid_#{recipient1.id}" }
  162. it 'resolves User from recipient list' do
  163. expect(trigger.assets({})[:User].keys).to include(recipient1.id)
  164. end
  165. end
  166. end
  167. end
  168. context 'recipient group keyword only' do
  169. let(:recipient) { 'ticket_customer' }
  170. it 'contains matching recipient' do
  171. expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s)
  172. end
  173. end
  174. end
  175. context 'active S/MIME integration' do
  176. before do
  177. Setting.set('smime_integration', true)
  178. create(:smime_certificate, :with_private, fixture: system_email_address)
  179. create(:smime_certificate, fixture: customer_email_address)
  180. end
  181. let(:system_email_address) { 'smime1@example.com' }
  182. let(:customer_email_address) { 'smime2@example.com' }
  183. let(:email_address) { create(:email_address, email: system_email_address) }
  184. let(:group) { create(:group, email_address: email_address) }
  185. let(:customer) { create(:customer, email: customer_email_address) }
  186. let(:security_preferences) { Ticket::Article.last.preferences[:security] }
  187. let(:perform) do
  188. {
  189. 'notification.email' => {
  190. 'recipient' => 'ticket_customer',
  191. 'subject' => 'Subject dummy.',
  192. 'body' => 'Body dummy.',
  193. }.merge(security_configuration)
  194. }
  195. end
  196. let!(:ticket) { create(:ticket, group: group, customer: customer) }
  197. context 'sending articles' do
  198. before do
  199. TransactionDispatcher.commit
  200. end
  201. context 'expired certificate' do
  202. let(:system_email_address) { 'expiredsmime1@example.com' }
  203. let(:security_configuration) do
  204. {
  205. 'sign' => 'always',
  206. 'encryption' => 'always',
  207. }
  208. end
  209. it 'creates unsigned article' do
  210. expect(security_preferences[:sign][:success]).to be false
  211. expect(security_preferences[:encryption][:success]).to be true
  212. end
  213. end
  214. context 'sign and encryption not set' do
  215. let(:security_configuration) { {} }
  216. it 'does not sign or encrypt' do
  217. expect(security_preferences[:sign][:success]).to be false
  218. expect(security_preferences[:encryption][:success]).to be false
  219. end
  220. end
  221. context 'sign and encryption disabled' do
  222. let(:security_configuration) do
  223. {
  224. 'sign' => 'no',
  225. 'encryption' => 'no',
  226. }
  227. end
  228. it 'does not sign or encrypt' do
  229. expect(security_preferences[:sign][:success]).to be false
  230. expect(security_preferences[:encryption][:success]).to be false
  231. end
  232. end
  233. context 'sign is enabled' do
  234. let(:security_configuration) do
  235. {
  236. 'sign' => 'always',
  237. 'encryption' => 'no',
  238. }
  239. end
  240. it 'signs' do
  241. expect(security_preferences[:sign][:success]).to be true
  242. expect(security_preferences[:encryption][:success]).to be false
  243. end
  244. end
  245. context 'encryption enabled' do
  246. let(:security_configuration) do
  247. {
  248. 'sign' => 'no',
  249. 'encryption' => 'always',
  250. }
  251. end
  252. it 'encrypts' do
  253. expect(security_preferences[:sign][:success]).to be false
  254. expect(security_preferences[:encryption][:success]).to be true
  255. end
  256. end
  257. context 'sign and encryption enabled' do
  258. let(:security_configuration) do
  259. {
  260. 'sign' => 'always',
  261. 'encryption' => 'always',
  262. }
  263. end
  264. it 'signs and encrypts' do
  265. expect(security_preferences[:sign][:success]).to be true
  266. expect(security_preferences[:encryption][:success]).to be true
  267. end
  268. end
  269. end
  270. context 'discard' do
  271. context 'sign' do
  272. let(:security_configuration) do
  273. {
  274. 'sign' => 'discard',
  275. }
  276. end
  277. context 'group without certificate' do
  278. let(:group) { create(:group) }
  279. it 'does not fire' do
  280. expect { TransactionDispatcher.commit }
  281. .to change(Ticket::Article, :count).by(0)
  282. end
  283. end
  284. end
  285. context 'encryption' do
  286. let(:security_configuration) do
  287. {
  288. 'encryption' => 'discard',
  289. }
  290. end
  291. context 'customer without certificate' do
  292. let(:customer) { create(:customer) }
  293. it 'does not fire' do
  294. expect { TransactionDispatcher.commit }
  295. .to change(Ticket::Article, :count).by(0)
  296. end
  297. end
  298. end
  299. context 'mixed' do
  300. context 'sign' do
  301. let(:security_configuration) do
  302. {
  303. 'encryption' => 'always',
  304. 'sign' => 'discard',
  305. }
  306. end
  307. context 'group without certificate' do
  308. let(:group) { create(:group) }
  309. it 'does not fire' do
  310. expect { TransactionDispatcher.commit }
  311. .to change(Ticket::Article, :count).by(0)
  312. end
  313. end
  314. end
  315. context 'encryption' do
  316. let(:security_configuration) do
  317. {
  318. 'encryption' => 'discard',
  319. 'sign' => 'always',
  320. }
  321. end
  322. context 'customer without certificate' do
  323. let(:customer) { create(:customer) }
  324. it 'does not fire' do
  325. expect { TransactionDispatcher.commit }
  326. .to change(Ticket::Article, :count).by(0)
  327. end
  328. end
  329. end
  330. end
  331. end
  332. end
  333. end
  334. context 'for condition "ticket updated"' do
  335. let(:condition) do
  336. { 'ticket.action' => { 'operator' => 'is', 'value' => 'update' } }
  337. end
  338. let!(:ticket) { create(:ticket).tap { TransactionDispatcher.commit } }
  339. context 'when new article is created directly' do
  340. context 'with empty #preferences hash' do
  341. let!(:article) { create(:ticket_article, ticket: ticket) }
  342. it 'fires (without altering ticket state)' do
  343. expect { TransactionDispatcher.commit }
  344. .to change { ticket.reload.articles.count }.by(1)
  345. .and not_change { ticket.reload.state.name }.from('new')
  346. end
  347. end
  348. context 'with #preferences { "send-auto-response" => false }' do
  349. let!(:article) do
  350. create(:ticket_article,
  351. ticket: ticket,
  352. preferences: { 'send-auto-response' => false })
  353. end
  354. it 'does not fire' do
  355. expect { TransactionDispatcher.commit }
  356. .not_to change { ticket.reload.articles.count }
  357. end
  358. end
  359. end
  360. context 'when new article is created via Channel::EmailParser.process' do
  361. context 'with a regular message' do
  362. let!(:article) do
  363. create(:ticket_article,
  364. ticket: ticket,
  365. message_id: raw_email[%r{(?<=^References: )\S*}],
  366. subject: raw_email[%r{(?<=^Subject: Re: ).*$}])
  367. end
  368. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail005.box')) }
  369. it 'fires (without altering ticket state)' do
  370. expect { Channel::EmailParser.new.process({}, raw_email) }
  371. .to not_change { Ticket.count }
  372. .and change { ticket.reload.articles.count }.by(2)
  373. .and not_change { ticket.reload.state.name }.from('new')
  374. end
  375. end
  376. context 'with delivery-failed "bounce message"' do
  377. let!(:article) do
  378. create(:ticket_article,
  379. ticket: ticket,
  380. message_id: raw_email[%r{(?<=^Message-ID: )\S*}])
  381. end
  382. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail055.box')) }
  383. it 'does not fire' do
  384. expect { Channel::EmailParser.new.process({}, raw_email) }
  385. .to change { ticket.reload.articles.count }.by(1)
  386. end
  387. end
  388. end
  389. end
  390. context 'with condition execution_time.calendar_id' do
  391. let(:calendar) { create(:calendar) }
  392. let(:perform) do
  393. { 'ticket.title'=>{ 'value'=>'triggered' } }
  394. end
  395. let!(:ticket) { create(:ticket, title: 'Test Ticket') }
  396. context 'is in working time' do
  397. let(:condition) do
  398. { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) }, 'execution_time.calendar_id' => { 'operator' => 'is in working time', 'value' => calendar.id } }
  399. end
  400. it 'does trigger only in working time' do
  401. travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
  402. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  403. end
  404. it 'does not trigger out of working time' do
  405. travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
  406. TransactionDispatcher.commit
  407. expect(ticket.reload.title).to eq('Test Ticket')
  408. end
  409. end
  410. context 'is not in working time' do
  411. let(:condition) do
  412. { 'execution_time.calendar_id' => { 'operator' => 'is not in working time', 'value' => calendar.id } }
  413. end
  414. it 'does not trigger in working time' do
  415. travel_to Time.zone.parse('2020-02-12T12:00:00Z0')
  416. TransactionDispatcher.commit
  417. expect(ticket.reload.title).to eq('Test Ticket')
  418. end
  419. it 'does trigger out of working time' do
  420. travel_to Time.zone.parse('2020-02-12T02:00:00Z0')
  421. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  422. end
  423. end
  424. end
  425. context 'with article last sender equals system address' do
  426. let!(:ticket) { create(:ticket) }
  427. let(:perform) do
  428. {
  429. 'notification.email' => {
  430. 'recipient' => 'article_last_sender',
  431. 'subject' => 'foo last sender',
  432. 'body' => 'some body with &gt;snip&lt;#{article.body_as_html}&gt;/snip&lt;', # rubocop:disable Lint/InterpolationCheck
  433. }
  434. }
  435. end
  436. let(:condition) do
  437. { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) } }
  438. end
  439. let!(:system_address) do
  440. create(:email_address)
  441. end
  442. context 'article with from equal to the a system address' do
  443. let!(:article) do
  444. create(:ticket_article,
  445. ticket: ticket,
  446. from: system_address.email,)
  447. end
  448. it 'does not trigger because of the last article is created my system address' do
  449. expect { TransactionDispatcher.commit }.to change { ticket.reload.articles.count }.by(0)
  450. expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender')
  451. expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email)
  452. end
  453. end
  454. context 'article with reply_to equal to the a system address' do
  455. let!(:article) do
  456. create(:ticket_article,
  457. ticket: ticket,
  458. from: system_address.email,
  459. reply_to: system_address.email,)
  460. end
  461. it 'does not trigger because of the last article is created my system address' do
  462. expect { TransactionDispatcher.commit }.to change { ticket.reload.articles.count }.by(0)
  463. expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender')
  464. expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email)
  465. end
  466. end
  467. end
  468. end
  469. context 'with pre condition current_user.id' do
  470. let(:perform) do
  471. { 'ticket.title'=>{ 'value'=>'triggered' } }
  472. end
  473. let(:user) do
  474. user = create(:agent)
  475. user.roles.first.groups << group
  476. user
  477. end
  478. let(:group) { Group.first }
  479. let(:ticket) do
  480. create(:ticket,
  481. title: 'Test Ticket', group: group,
  482. owner_id: user.id, created_by_id: user.id, updated_by_id: user.id)
  483. end
  484. shared_examples 'successful trigger' do |attribute:|
  485. let(:attribute) { attribute }
  486. let(:condition) do
  487. { attribute => { operator: 'is', pre_condition: 'current_user.id', value: '', value_completion: '' } }
  488. end
  489. it "for #{attribute}" do
  490. ticket && trigger
  491. expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered')
  492. end
  493. end
  494. it_behaves_like 'successful trigger', attribute: 'ticket.updated_by_id'
  495. it_behaves_like 'successful trigger', attribute: 'ticket.owner_id'
  496. end
  497. describe 'Multi-trigger interactions:' do
  498. let(:ticket) { create(:ticket) }
  499. context 'cascading (i.e., trigger A satisfies trigger B satisfies trigger C)' do
  500. subject!(:triggers) do
  501. [
  502. create(:trigger, condition: initial_state, perform: first_change, name: 'A'),
  503. create(:trigger, condition: first_change, perform: second_change, name: 'B'),
  504. create(:trigger, condition: second_change, perform: third_change, name: 'C')
  505. ]
  506. end
  507. context 'in a chain' do
  508. let(:initial_state) do
  509. {
  510. 'ticket.state_id' => {
  511. 'operator' => 'is',
  512. 'value' => Ticket::State.lookup(name: 'new').id.to_s,
  513. }
  514. }
  515. end
  516. let(:first_change) do
  517. {
  518. 'ticket.state_id' => {
  519. 'operator' => 'is',
  520. 'value' => Ticket::State.lookup(name: 'open').id.to_s,
  521. }
  522. }
  523. end
  524. let(:second_change) do
  525. {
  526. 'ticket.state_id' => {
  527. 'operator' => 'is',
  528. 'value' => Ticket::State.lookup(name: 'closed').id.to_s,
  529. }
  530. }
  531. end
  532. let(:third_change) do
  533. {
  534. 'ticket.state_id' => {
  535. 'operator' => 'is',
  536. 'value' => Ticket::State.lookup(name: 'merged').id.to_s,
  537. }
  538. }
  539. end
  540. context 'in alphabetical order (by name)' do
  541. it 'fires all triggers in sequence' do
  542. expect { TransactionDispatcher.commit }
  543. .to change { ticket.reload.state.name }.to('merged')
  544. end
  545. end
  546. context 'out of alphabetical order (by name)' do
  547. before do
  548. triggers.first.update(name: 'E')
  549. triggers.second.update(name: 'F')
  550. triggers.third.update(name: 'D')
  551. end
  552. context 'with Setting ticket_trigger_recursive: true' do
  553. before { Setting.set('ticket_trigger_recursive', true) }
  554. it 'evaluates triggers in sequence, then loops back to the start and re-evalutes skipped triggers' do
  555. expect { TransactionDispatcher.commit }
  556. .to change { ticket.reload.state.name }.to('merged')
  557. end
  558. end
  559. context 'with Setting ticket_trigger_recursive: false' do
  560. before { Setting.set('ticket_trigger_recursive', false) }
  561. it 'evaluates triggers in sequence, firing only the ones that match' do
  562. expect { TransactionDispatcher.commit }
  563. .to change { ticket.reload.state.name }.to('closed')
  564. end
  565. end
  566. end
  567. end
  568. context 'in circular reference (i.e., trigger A satisfies trigger B satisfies trigger C satisfies trigger A...)' do
  569. let(:initial_state) do
  570. {
  571. 'ticket.priority_id' => {
  572. 'operator' => 'is',
  573. 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s,
  574. }
  575. }
  576. end
  577. let(:first_change) do
  578. {
  579. 'ticket.priority_id' => {
  580. 'operator' => 'is',
  581. 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s,
  582. }
  583. }
  584. end
  585. let(:second_change) do
  586. {
  587. 'ticket.priority_id' => {
  588. 'operator' => 'is',
  589. 'value' => Ticket::Priority.lookup(name: '1 low').id.to_s,
  590. }
  591. }
  592. end
  593. let(:third_change) do
  594. {
  595. 'ticket.priority_id' => {
  596. 'operator' => 'is',
  597. 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s,
  598. }
  599. }
  600. end
  601. context 'with Setting ticket_trigger_recursive: true' do
  602. before { Setting.set('ticket_trigger_recursive', true) }
  603. it 'fires each trigger once, without being caught in an endless loop' do
  604. expect { Timeout.timeout(2) { TransactionDispatcher.commit } }
  605. .to not_change { ticket.reload.priority.name }
  606. .and not_raise_error
  607. end
  608. end
  609. context 'with Setting ticket_trigger_recursive: false' do
  610. before { Setting.set('ticket_trigger_recursive', false) }
  611. it 'fires each trigger once, without being caught in an endless loop' do
  612. expect { Timeout.timeout(2) { TransactionDispatcher.commit } }
  613. .to not_change { ticket.reload.priority.name }
  614. .and not_raise_error
  615. end
  616. end
  617. end
  618. end
  619. context 'competing (i.e., trigger A un-satisfies trigger B)' do
  620. subject!(:triggers) do
  621. [
  622. create(:trigger, condition: initial_state, perform: change_a, name: 'A'),
  623. create(:trigger, condition: initial_state, perform: change_b, name: 'B')
  624. ]
  625. end
  626. let(:initial_state) do
  627. {
  628. 'ticket.state_id' => {
  629. 'operator' => 'is',
  630. 'value' => Ticket::State.lookup(name: 'new').id.to_s,
  631. }
  632. }
  633. end
  634. let(:change_a) do
  635. {
  636. 'ticket.state_id' => {
  637. 'operator' => 'is',
  638. 'value' => Ticket::State.lookup(name: 'open').id.to_s,
  639. }
  640. }
  641. end
  642. let(:change_b) do
  643. {
  644. 'ticket.priority_id' => {
  645. 'operator' => 'is',
  646. 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s,
  647. }
  648. }
  649. end
  650. it 'evaluates triggers in sequence, firing only the ones that match' do
  651. expect { TransactionDispatcher.commit }
  652. .to change { ticket.reload.state.name }.to('open')
  653. .and not_change { ticket.reload.priority.name }
  654. end
  655. end
  656. end
  657. end