perform_changes_spec.rb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'models/concerns/can_perform_changes_examples'
  4. RSpec.describe 'Ticket::PerformChanges', :aggregate_failures do
  5. subject(:object) { create(:ticket, group: group, owner: create(:agent, groups: [group])) }
  6. let(:group) { create(:group) }
  7. let(:performable) do
  8. create(:trigger, perform: perform, activator: 'action', execution_condition_mode: 'always', condition: { 'ticket.state_id'=>{ 'operator' => 'is', 'value' => Ticket::State.pluck(:id) } })
  9. end
  10. include_examples 'CanPerformChanges', object_name: 'Ticket'
  11. context 'when invalid data is given' do
  12. context 'with not existing attribute' do
  13. let(:perform) do
  14. {
  15. 'ticket.foobar' => {
  16. 'value' => 'dummy',
  17. }
  18. }
  19. end
  20. it 'raises an error' do
  21. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  22. .to raise_error(RuntimeError, 'The given trigger contains invalid attributes, stopping!')
  23. end
  24. end
  25. context 'with invalid action in "perform" hash' do
  26. let(:perform) do
  27. {
  28. 'dummy' => {
  29. 'value' => 'delete',
  30. }
  31. }
  32. end
  33. it 'raises an error' do
  34. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  35. .to raise_error(RuntimeError, 'The given trigger contains no valid actions, stopping!')
  36. end
  37. end
  38. end
  39. # Regression test for https://github.com/zammad/zammad/issues/2001
  40. describe 'argument handling' do
  41. let(:perform) do
  42. {
  43. 'notification.email' => {
  44. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  45. recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
  46. subject: "Autoclose (\#{ticket.title})"
  47. }
  48. }
  49. end
  50. it 'does not mutate contents of "perform" hash' do
  51. expect { object.perform_changes(performable, 'trigger', {}, 1) }
  52. .not_to change { perform }
  53. end
  54. end
  55. context 'with "ticket.state_id" key in "perform" hash' do
  56. let(:perform) do
  57. {
  58. 'ticket.state_id' => {
  59. 'value' => Ticket::State.lookup(name: 'closed').id
  60. }
  61. }
  62. end
  63. it 'changes #state to specified value' do
  64. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  65. .to change { object.reload.state.name }.to('closed')
  66. end
  67. end
  68. # Test for backwards compatibility after PR https://github.com/zammad/zammad/pull/2862
  69. context 'with "pending_time" => { "value": DATE } in "perform" hash' do
  70. let(:perform) do
  71. {
  72. 'ticket.state_id' => {
  73. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  74. },
  75. 'ticket.pending_time' => {
  76. 'value' => timestamp,
  77. },
  78. }
  79. end
  80. let(:timestamp) { Time.zone.now }
  81. it 'changes pending date to given date' do
  82. freeze_time do
  83. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  84. .to change(object, :pending_time)
  85. .to timestamp.change(sec: 0)
  86. end
  87. end
  88. end
  89. # Test for PR https://github.com/zammad/zammad/pull/2862
  90. context 'with "pending_time" => { "operator": "relative" } in "perform" hash' do
  91. shared_examples 'verify' do
  92. it 'verify relative pending time rule' do
  93. freeze_time do
  94. target_time = relative_value
  95. .send(relative_range)
  96. .from_now
  97. .change(sec: 0)
  98. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  99. .to change(object, :pending_time)
  100. .to target_time
  101. end
  102. end
  103. end
  104. let(:perform) do
  105. {
  106. 'ticket.state_id' => {
  107. 'value' => Ticket::State.lookup(name: 'pending reminder').id.to_s
  108. },
  109. 'ticket.pending_time' => {
  110. 'operator' => 'relative',
  111. 'value' => relative_value,
  112. 'range' => relative_range_config
  113. },
  114. }
  115. end
  116. let(:relative_range_config) { relative_range.to_s.singularize }
  117. context 'when value in days' do
  118. let(:relative_value) { 2 }
  119. let(:relative_range) { :days }
  120. include_examples 'verify'
  121. end
  122. context 'when value in minutes' do
  123. let(:relative_value) { 60 }
  124. let(:relative_range) { :minutes }
  125. include_examples 'verify'
  126. end
  127. context 'when value in weeks' do
  128. let(:relative_value) { 2 }
  129. let(:relative_range) { :weeks }
  130. include_examples 'verify'
  131. end
  132. end
  133. context 'with tags in "perform" hash' do
  134. let(:user) { create(:agent, groups: [group]) }
  135. let(:perform) do
  136. {
  137. 'ticket.tags' => { 'operator' => tag_operator, 'value' => 'tag1, tag2' }
  138. }
  139. end
  140. context 'with add' do
  141. let(:tag_operator) { 'add' }
  142. it 'adds the tags' do
  143. expect { object.perform_changes(performable, 'trigger', object, user.id) }
  144. .to change { object.reload.tag_list }.to(%w[tag1 tag2])
  145. end
  146. end
  147. context 'with remove' do
  148. let(:tag_operator) { 'remove' }
  149. before do
  150. %w[tag1 tag2].each { |tag| object.tag_add(tag, 1) }
  151. end
  152. it 'removes the tags' do
  153. expect { object.perform_changes(performable, 'trigger', object, user.id) }
  154. .to change { object.reload.tag_list }.to([])
  155. end
  156. end
  157. end
  158. context 'with "pre_condition" in "perform" hash' do
  159. let(:user) { create(:agent, groups: [group]) }
  160. let(:perform) do
  161. {
  162. 'ticket.owner_id' => {
  163. 'pre_condition' => pre_condition,
  164. 'value' => value,
  165. 'value_completion' => '',
  166. }
  167. }
  168. end
  169. context 'with current_user.id' do
  170. let(:pre_condition) { 'current_user.id' }
  171. let(:value) { '' }
  172. it 'changes to specified value' do
  173. expect { object.perform_changes(performable, 'trigger', object, user.id) }
  174. .to change { object.reload.owner.id }.to(user.id)
  175. end
  176. end
  177. context 'with specific user' do
  178. let(:another_user) { create(:agent, groups: [group]) }
  179. let(:pre_condition) { 'specific' }
  180. let(:value) { another_user.id }
  181. it 'changes to specified value' do
  182. expect { object.perform_changes(performable, 'trigger', object, user.id) }
  183. .to change { object.reload.owner.id }.to(another_user.id)
  184. end
  185. end
  186. context 'with current_user.id, but missing user' do
  187. let(:pre_condition) { 'current_user.id' }
  188. let(:value) { '' }
  189. it 'raises an error' do
  190. expect { object.perform_changes(performable, 'trigger', object, nil) }
  191. .to raise_error(RuntimeError, "The required parameter 'user_id' is missing.")
  192. end
  193. end
  194. context 'with not_set' do
  195. let(:pre_condition) { 'not_set' }
  196. let(:value) { '' }
  197. it 'changes to user with id 1' do
  198. expect { object.perform_changes(performable, 'trigger', object, user.id) }
  199. .to change { object.reload.owner.id }.to(1)
  200. end
  201. end
  202. end
  203. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  204. let(:perform) do
  205. {
  206. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  207. 'ticket.action' => { 'value' => 'delete' },
  208. }
  209. end
  210. it 'performs a ticket deletion on a ticket' do
  211. expect { object.perform_changes(performable, 'trigger', object, User.first) }
  212. .to change(object, :destroyed?).to(true)
  213. end
  214. end
  215. context 'with a "notification.email" trigger' do
  216. # Regression test for https://github.com/zammad/zammad/issues/1543
  217. #
  218. # If a new article fires an email notification trigger,
  219. # and then another article is added to the same ticket
  220. # before that trigger is performed,
  221. # the email template's 'article' var should refer to the originating article,
  222. # not the newest one.
  223. #
  224. # (This occurs whenever one action fires multiple email notification triggers.)
  225. context 'when two articles are created before the trigger fires once (race condition)' do
  226. let!(:article) { create(:ticket_article, ticket: object) }
  227. let!(:new_article) { create(:ticket_article, ticket: object) }
  228. let(:trigger) do
  229. build(:trigger,
  230. perform: {
  231. 'notification.email' => {
  232. body: 'Sample notification',
  233. recipient: 'ticket_customer',
  234. subject: 'Sample subject'
  235. }
  236. })
  237. end
  238. let(:objects) do
  239. last_article = nil
  240. last_internal_article = nil
  241. last_external_article = nil
  242. all_articles = object.articles
  243. if article.nil?
  244. last_article = all_articles.last
  245. last_internal_article = all_articles.reverse.find(&:internal?)
  246. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  247. else
  248. last_article = article
  249. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  250. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  251. end
  252. {
  253. ticket: object,
  254. article: last_article,
  255. last_article: last_article,
  256. last_internal_article: last_internal_article,
  257. last_external_article: last_external_article,
  258. created_article: article,
  259. created_internal_article: article&.internal? ? article : nil,
  260. created_external_article: article&.internal? ? nil : article,
  261. }
  262. end
  263. # required by Ticket#perform_changes for email notifications
  264. before do
  265. allow(NotificationFactory::Mailer).to receive(:template).and_call_original
  266. article.ticket.group.update(email_address: create(:email_address))
  267. end
  268. it 'passes the first article to NotificationFactory::Mailer' do
  269. object.perform_changes(trigger, 'trigger', { article_id: article.id }, 1)
  270. expect(NotificationFactory::Mailer)
  271. .to have_received(:template)
  272. .with(hash_including(objects: objects))
  273. .at_least(:once)
  274. expect(NotificationFactory::Mailer)
  275. .not_to have_received(:template)
  276. .with(hash_including(objects: { ticket: object, article: new_article }))
  277. end
  278. end
  279. end
  280. context 'with a notification trigger' do
  281. # https://github.com/zammad/zammad/issues/2782
  282. #
  283. # Notification triggers should log notification as private or public
  284. # according to given configuration
  285. let(:user) { create(:admin, mobile: '+37061010000') }
  286. let(:perform) do
  287. {
  288. notification_key => {
  289. body: 'Old programmers never die. They just branch to a new address.',
  290. recipient: 'ticket_agents',
  291. subject: 'Old programmers never die. They just branch to a new address.'
  292. }
  293. }.deep_merge(additional_options).deep_stringify_keys
  294. end
  295. let(:notification_key) { "notification.#{notification_type}" }
  296. let!(:ticket_article) { create(:ticket_article, ticket: object) }
  297. let(:item) do
  298. {
  299. object: 'Ticket',
  300. object_id: object.id,
  301. user_id: user.id,
  302. type: 'update',
  303. article_id: ticket_article.id
  304. }
  305. end
  306. before { object.group.users << user }
  307. shared_examples 'verify log visibility status' do
  308. shared_examples 'notification trigger' do
  309. it 'adds Ticket::Article' do
  310. expect { object.perform_changes(performable, 'trigger', object, user) }
  311. .to change { object.articles.count }.by(1)
  312. end
  313. it 'new Ticket::Article visibility reflects setting' do
  314. object.perform_changes(performable, 'trigger', object, User.first)
  315. new_article = object.articles.reload.last
  316. expect(new_article.internal).to be target_internal_value
  317. end
  318. end
  319. context 'when set to private' do
  320. let(:additional_options) do
  321. {
  322. notification_key => {
  323. internal: true
  324. }
  325. }
  326. end
  327. let(:target_internal_value) { true }
  328. it_behaves_like 'notification trigger'
  329. end
  330. context 'when set to internal' do
  331. let(:additional_options) do
  332. {
  333. notification_key => {
  334. internal: false
  335. }
  336. }
  337. end
  338. let(:target_internal_value) { false }
  339. it_behaves_like 'notification trigger'
  340. end
  341. context 'when no selection was made' do # ensure previously created triggers default to public
  342. let(:additional_options) do
  343. {}
  344. end
  345. let(:target_internal_value) { false }
  346. it_behaves_like 'notification trigger'
  347. end
  348. end
  349. context 'when dispatching email' do
  350. let(:notification_type) { :email }
  351. include_examples 'verify log visibility status'
  352. end
  353. shared_examples 'add a new article' do
  354. it 'adds a new article' do
  355. expect { object.perform_changes(performable, 'trigger', item, user) }
  356. .to change { object.articles.count }.by(1)
  357. end
  358. end
  359. shared_examples 'add attachment to new article' do
  360. include_examples 'add a new article'
  361. it 'adds attachment to the new article' do
  362. object.perform_changes(performable, 'trigger', item, user)
  363. article = object.articles.reload.last
  364. expect(article.type.name).to eq('email')
  365. expect(article.sender.name).to eq('System')
  366. expect(article.attachments.count).to eq(1)
  367. expect(article.attachments[0].filename).to eq('some_file.pdf')
  368. expect(article.attachments[0].preferences['Content-ID']).to eq('image/pdf@01CAB192.K8H512Y9')
  369. end
  370. end
  371. shared_examples 'does not add attachment to new article' do
  372. include_examples 'add a new article'
  373. it 'does not add attachment to the new article' do
  374. object.perform_changes(performable, 'trigger', item, user)
  375. article = object.articles.reload.last
  376. expect(article.type.name).to eq('email')
  377. expect(article.sender.name).to eq('System')
  378. expect(article.attachments.count).to eq(0)
  379. end
  380. end
  381. context 'when dispatching email with include attachment present' do
  382. let(:notification_type) { :email }
  383. let(:additional_options) do
  384. {
  385. notification_key => {
  386. include_attachments: 'true'
  387. }
  388. }
  389. end
  390. context 'when ticket has an attachment' do
  391. before do
  392. UserInfo.current_user_id = 1
  393. create(:store,
  394. object: 'Ticket::Article',
  395. o_id: ticket_article.id,
  396. data: 'dGVzdCAxMjM=',
  397. filename: 'some_file.pdf',
  398. preferences: {
  399. 'Content-Type': 'image/pdf',
  400. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  401. })
  402. end
  403. include_examples 'add attachment to new article'
  404. end
  405. context 'when ticket does not have an attachment' do
  406. include_examples 'does not add attachment to new article'
  407. end
  408. end
  409. context 'when dispatching email with include attachment not present' do
  410. let(:notification_type) { :email }
  411. let(:additional_options) do
  412. {
  413. notification_key => {
  414. include_attachments: 'false'
  415. }
  416. }
  417. end
  418. context 'when ticket has an attachment' do
  419. before do
  420. UserInfo.current_user_id = 1
  421. create(:store,
  422. object: 'Ticket::Article',
  423. o_id: ticket_article.id,
  424. data: 'dGVzdCAxMjM=',
  425. filename: 'some_file.pdf',
  426. preferences: {
  427. 'Content-Type': 'image/pdf',
  428. 'Content-ID': 'image/pdf@01CAB192.K8H512Y9',
  429. })
  430. end
  431. include_examples 'does not add attachment to new article'
  432. end
  433. context 'when ticket does not have an attachment' do
  434. include_examples 'does not add attachment to new article'
  435. end
  436. end
  437. context 'when dispatching SMS' do
  438. let(:notification_type) { :sms }
  439. before { create(:channel, area: 'Sms::Notification') }
  440. include_examples 'verify log visibility status'
  441. end
  442. end
  443. context 'with a "notification.webhook" trigger', performs_jobs: true do
  444. let(:webhook) { create(:webhook, endpoint: 'http://api.example.com/webhook', signature_token: '53CR3t') }
  445. let(:trigger) do
  446. create(:trigger,
  447. perform: {
  448. 'notification.webhook' => { 'webhook_id' => webhook.id }
  449. })
  450. end
  451. let(:context_data) do
  452. {
  453. type: 'info',
  454. execution: 'trigger',
  455. changes: { 'state_id' => %w[2 4] },
  456. user_id: 1,
  457. }
  458. end
  459. it 'schedules the webhooks notification job' do
  460. expect { object.perform_changes(trigger, 'trigger', context_data, 1) }.to have_enqueued_job(TriggerWebhookJob).with(
  461. trigger,
  462. object,
  463. nil,
  464. changes: { 'State' => %w[open closed] },
  465. user_id: 1,
  466. execution_type: 'trigger',
  467. event_type: 'info',
  468. )
  469. end
  470. end
  471. context 'with a "article.note" trigger' do
  472. let(:user) { create(:agent, groups: [group]) }
  473. let(:perform) do
  474. { 'article.note' => { 'subject' => 'Test subject note', 'internal' => 'true', 'body' => 'Test body note' } }
  475. end
  476. it 'adds the note' do
  477. object.perform_changes(performable, 'trigger', object, user.id)
  478. expect(object.articles.reload.last).to have_attributes(
  479. subject: 'Test subject note',
  480. body: 'Test body note',
  481. internal: true,
  482. )
  483. end
  484. end
  485. describe 'Check if blocking notifications works' do
  486. context 'when mail delivery failed' do
  487. let(:ticket) { create(:ticket) }
  488. let(:customer) { create(:customer) }
  489. let(:perform) do
  490. {
  491. 'notification.email' => {
  492. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  493. recipient: ["userid_#{customer.id}"],
  494. subject: "Autoclose (\#{ticket.title})",
  495. }
  496. }
  497. end
  498. context 'with a normal user' do
  499. it 'sends trigger base notification' do
  500. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }.to change { ticket.reload.articles.count }.by(1)
  501. end
  502. end
  503. context 'with a permanent failed user' do
  504. let(:failed_date) { 1.second.ago }
  505. let(:customer) do
  506. user = create(:customer)
  507. user.preferences.merge!(mail_delivery_failed: true, mail_delivery_failed_data: failed_date)
  508. user.save!
  509. user
  510. end
  511. it 'sends no trigger base notification' do
  512. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }.not_to change { ticket.reload.articles.count }
  513. expect(customer.reload.preferences).to include(
  514. mail_delivery_failed: true,
  515. mail_delivery_failed_data: failed_date,
  516. )
  517. end
  518. context 'with failed date 61 days ago' do
  519. let(:failed_date) { 61.days.ago }
  520. it 'sends trigger base notification' do
  521. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }.to change { ticket.reload.articles.count }.by(1)
  522. expect(customer.reload.preferences).to include(
  523. mail_delivery_failed: false,
  524. mail_delivery_failed_data: nil,
  525. )
  526. end
  527. end
  528. context 'with failed date 70 days ago' do
  529. let(:failed_date) { 70.days.ago }
  530. it 'sends trigger base notification' do
  531. expect { ticket.perform_changes(performable, 'trigger', ticket, User.first) }.to change { ticket.reload.articles.count }.by(1)
  532. expect(customer.reload.preferences).to include(
  533. mail_delivery_failed: false,
  534. mail_delivery_failed_data: nil,
  535. )
  536. end
  537. end
  538. end
  539. end
  540. end
  541. end