custom_payload_spec.rb 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe TriggerWebhookJob::CustomPayload do
  4. # rubocop:disable Lint/InterpolationCheck
  5. describe '.generate' do
  6. subject(:generate) { described_class.generate(record, { ticket:, article:, notification: }) }
  7. let(:ticket) { create(:ticket) }
  8. let(:article) { create(:ticket_article, body: "Text with\nnew line.") }
  9. let(:event) do
  10. {
  11. type: 'info',
  12. execution: 'trigger',
  13. changes: { 'state' => %w[open closed] },
  14. user_id: 1,
  15. }
  16. end
  17. let(:notification) { TriggerWebhookJob::CustomPayload::Track::Notification.generate({ ticket:, article: }, { event: }) }
  18. context 'when the payload is empty' do
  19. let(:record) { {}.to_json }
  20. let(:json_data) { {} }
  21. it 'returns an empty JSON object' do
  22. expect(generate).to eq(json_data)
  23. end
  24. end
  25. context 'when the placeholder is empty' do
  26. let(:record) { { 'ticket' => '#{}' }.to_json }
  27. let(:json_data) { { 'ticket' => '#{}' } }
  28. it 'returns the placeholder' do
  29. expect(generate).to eq(json_data)
  30. end
  31. end
  32. context 'when the placeholder is invalid' do
  33. let(:record) { { 'ticket' => '#{ticket.title', 'article' => '#{article.id article.note}' }.to_json }
  34. let(:json_data) { { 'ticket' => '#{ticket.title', 'article' => '#{article.id article.note}' } }
  35. it 'returns the placeholder' do
  36. expect(generate).to eq(json_data)
  37. end
  38. end
  39. context 'when the placeholder base object ticket or article is missing' do
  40. let(:record) { { 'ticket' => '#{.title}' }.to_json }
  41. let(:json_data) { { 'ticket' => '#{no object provided}' } }
  42. it 'returns the placeholder reporting "no object provided"' do
  43. expect(generate).to eq(json_data)
  44. end
  45. end
  46. context 'when the placeholder base object is other than ticket or article' do
  47. let(:record) { { 'user' => '#{user}' }.to_json }
  48. let(:json_data) { { 'user' => '#{user / no such object}' } }
  49. it 'returns the placehodler reporting "no such object"' do
  50. expect(generate).to eq(json_data)
  51. end
  52. end
  53. context 'when the placeholder contains only base object ticket or article' do
  54. let(:record) { { 'ticket' => '#{ticket}', 'Article' => '#{article}' }.to_json }
  55. let(:json_data) { { 'ticket' => '#{ticket / missing method}', 'Article' => '#{article / missing method}' } }
  56. it 'returns the placeholder reporting "missing method"' do
  57. expect(generate).to eq(json_data)
  58. end
  59. end
  60. context 'when the placeholder contains denied method' do
  61. let(:record) { { 'ticket' => '#{ticket.articles}' }.to_json }
  62. let(:json_data) { { 'ticket' => '#{ticket.articles / no such method}' } }
  63. it 'returns the placeholder reporting "no such method"' do
  64. expect(generate).to eq(json_data)
  65. end
  66. end
  67. context 'when the placeholder contains denied attribute' do
  68. let(:record) { { 'ticket.owner' => '#{ticket.owner.password}' }.to_json }
  69. let(:json_data) { { 'ticket.owner' => '#{ticket.owner.password / no such method}' } }
  70. it 'returns the placeholder reporting "no such method"' do
  71. expect(generate).to eq(json_data)
  72. end
  73. end
  74. context 'when the placeholder contains danger method' do
  75. let(:record) { { 'ticket.owner' => '#{ticket.destroy!}' }.to_json }
  76. let(:json_data) { { 'ticket.owner' => '#{ticket.destroy! / no such method}' } }
  77. it 'returns the placeholder reporting "no such method"' do
  78. expect(generate).to eq(json_data)
  79. end
  80. end
  81. context 'when the placeholder ends with complex object' do
  82. let(:record) { { 'ticket' => '#{ticket.group}' }.to_json }
  83. let(:json_data) { { 'ticket' => '#{ticket.group / no such method}' } }
  84. it 'returns the placeholder reporting "no such method"' do
  85. expect(generate).to eq(json_data)
  86. end
  87. end
  88. context 'when the placeholder contains valid object and invalid method' do
  89. let(:record) { { 'ticket' => '#{ticket.1_article}' }.to_json }
  90. let(:json_data) { { 'ticket' => '#{ticket.1_article / no such method}' } }
  91. it 'returns the placeholder reporting "no such method"' do
  92. expect(generate).to eq(json_data)
  93. end
  94. end
  95. context 'when the placeholder contains valid object and method' do
  96. let(:record) { { 'ticket.id' => '#{ticket.id}' }.to_json }
  97. let(:json_data) { { 'ticket.id' => ticket.id } }
  98. it 'returns the determined value' do
  99. expect(generate).to eq(json_data)
  100. end
  101. end
  102. context 'when the placeholder contains valid object and method, but the value is nil' do
  103. let(:record) do
  104. {
  105. 'ticket.organization.name' => '#{ticket.organization.name}',
  106. 'ticket.title' => '#{ticket.title}'
  107. }.to_json
  108. end
  109. let(:json_data) do
  110. {
  111. 'ticket.organization.name' => '',
  112. 'ticket.title' => ticket.title
  113. }
  114. end
  115. it 'returns an empty string' do
  116. expect(generate).to eq(json_data)
  117. end
  118. end
  119. context 'when the placeholder contains multiple valid object and method' do
  120. let(:record) do
  121. {
  122. 'ticket' => { 'owner' => '#{ticket.owner.fullname}' },
  123. 'article' => { 'created_at' => '#{article.created_at}' }
  124. }.to_json
  125. end
  126. let(:json_data) do
  127. {
  128. 'ticket' => { 'owner' => ticket.owner.fullname.to_s },
  129. 'article' => { 'created_at' => article.created_at.to_s }
  130. }
  131. end
  132. it 'returns the determined value' do
  133. expect(generate).to eq(json_data)
  134. end
  135. end
  136. context 'when the placeholder contains multiple attributes' do
  137. let(:record) do
  138. {
  139. 'my_field' => 'Test #{ticket.id} // #{ticket.group.name} Test',
  140. 'my_field2' => '#{ticket.id} // #{ticket.group.name} Test',
  141. 'my_field3' => '#{ticket.id}',
  142. 'my_field4' => '#{ticket.group.name}',
  143. }.to_json
  144. end
  145. let(:json_data) do
  146. {
  147. 'my_field' => "Test #{ticket.id} // #{ticket.group.name} Test",
  148. 'my_field2' => "#{ticket.id} // #{ticket.group.name} Test",
  149. 'my_field3' => ticket.id,
  150. 'my_field4' => ticket.group.name.to_s,
  151. }
  152. end
  153. it 'returns the placeholder reporting "no such method"' do
  154. expect(generate).to eq(json_data)
  155. end
  156. end
  157. context 'when the payload contains a complex structure' do
  158. let(:record) do
  159. {
  160. 'current_user' => '#{current_user.fullname}',
  161. 'ticket' => {
  162. 'id' => '#{ticket.id}',
  163. 'owner' => '#{ticket.owner.fullname}',
  164. 'group' => '#{ticket.group.name}',
  165. 'article' => {
  166. 'id' => '#{article.id}',
  167. 'created_at' => '#{article.created_at}',
  168. 'subject' => '#{article.subject}',
  169. 'body' => '#{article.body}',
  170. 'attachments' => '#{article.attachments}',
  171. 'internal' => '#{article.internal}',
  172. }
  173. }
  174. }.to_json
  175. end
  176. let(:json_data) do
  177. {
  178. 'current_user' => '#{current_user / no such object}',
  179. 'ticket' => {
  180. 'id' => ticket.id,
  181. 'owner' => ticket.owner.fullname.to_s,
  182. 'group' => ticket.group.name.to_s,
  183. 'article' => {
  184. 'id' => article.id,
  185. 'created_at' => article.created_at.to_s,
  186. 'subject' => article.subject.to_s,
  187. 'body' => article.body.to_s,
  188. 'attachments' => '#{article.attachments / no such method}',
  189. 'internal' => article.internal
  190. }
  191. }
  192. }
  193. end
  194. it 'returns a valid JSON payload' do
  195. expect(generate).to eq(json_data)
  196. end
  197. end
  198. context 'when the replacement value contains double quotes' do
  199. let(:ticket) { create(:ticket, title: 'Test "Title"') }
  200. let(:record) { { 'ticket.title' => '#{ticket.title}' }.to_json }
  201. let(:json_data) { { 'ticket.title' => 'Test "Title"' } }
  202. it 'returns the determined value' do
  203. expect(generate).to eq(json_data)
  204. end
  205. end
  206. context 'when object attributes are used in the placeholder', db_strategy: :reset do
  207. let(:ticket) { create(:ticket, object_manager_attribute_name => object_manager_attribute_value) }
  208. before do
  209. create_object_manager_attribute
  210. ObjectManager::Attribute.migration_execute
  211. end
  212. shared_examples 'check different usage' do
  213. context 'when used in string context' do
  214. let(:record) do
  215. {
  216. "ticket.#{object_manager_attribute_name}" => "Test \#{ticket.#{object_manager_attribute_name}}"
  217. }.to_json
  218. end
  219. let(:json_data) do
  220. {
  221. "ticket.#{object_manager_attribute_name}" => "Test #{object_manager_attribute_value}"
  222. }
  223. end
  224. it 'returns the determined value' do
  225. expect(generate).to eq(json_data)
  226. end
  227. end
  228. context 'when used in direct context' do
  229. let(:record) do
  230. {
  231. "ticket.#{object_manager_attribute_name}" => "\#{ticket.#{object_manager_attribute_name}}"
  232. }.to_json
  233. end
  234. let(:json_data) do
  235. {
  236. "ticket.#{object_manager_attribute_name}" => object_manager_attribute_value
  237. }
  238. end
  239. it 'returns the determined value' do
  240. expect(generate).to eq(json_data)
  241. end
  242. end
  243. end
  244. context 'when multiselect is used inside the ticket' do
  245. let(:object_manager_attribute_name) { 'multiselect_keys_001' }
  246. let(:object_manager_attribute_value) { %w[key_1 key_3] }
  247. let(:create_object_manager_attribute) do
  248. create(:object_manager_attribute_multiselect, name: object_manager_attribute_name)
  249. end
  250. include_examples 'check different usage'
  251. end
  252. context 'when external data source is used inside the ticket', db_adapter: :postgresql do
  253. let(:object_manager_attribute_name) { 'autocompletion_ajax_external_data_source' }
  254. let(:object_manager_attribute_value) do
  255. {
  256. 'value' => 123,
  257. 'label' => 'Example',
  258. }
  259. end
  260. let(:create_object_manager_attribute) do
  261. create(:object_manager_attribute_autocompletion_ajax_external_data_source, name: object_manager_attribute_name)
  262. end
  263. include_examples 'check different usage'
  264. end
  265. end
  266. describe "when the placeholder contains object 'notification'" do
  267. let(:record) do
  268. {
  269. 'subject' => '#{notification.subject}',
  270. 'message' => '#{notification.message}',
  271. 'changes' => '#{notification.changes}',
  272. 'body' => '#{notification.body}',
  273. 'link' => '#{notification.link}',
  274. }.to_json
  275. end
  276. context "when the event is of the type 'create'" do
  277. let(:event) do
  278. {
  279. type: 'create',
  280. execution: 'trigger',
  281. user_id: 1,
  282. }
  283. end
  284. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  285. expect(generate['subject']).to eq(ticket.title)
  286. expect(generate['body']).to eq(article.body_as_text)
  287. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  288. expect(generate['message']).to include('Created by')
  289. expect(generate['changes']).to include('State: new')
  290. end
  291. end
  292. context "when the event is of the type 'update'" do
  293. let(:event) do
  294. {
  295. type: 'update',
  296. execution: 'trigger',
  297. changes: { 'state' => %w[open closed] },
  298. user_id: 1,
  299. }
  300. end
  301. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  302. expect(generate['subject']).to eq(ticket.title)
  303. expect(generate['body']).to eq(article.body_as_text)
  304. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  305. expect(generate['message']).to include('Updated by')
  306. expect(generate['changes']).to include('state: open -> closed')
  307. end
  308. context 'without changes' do
  309. let(:event) do
  310. {
  311. type: 'update',
  312. execution: 'trigger',
  313. user_id: 1,
  314. }
  315. end
  316. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  317. expect(generate['subject']).to eq(ticket.title)
  318. expect(generate['body']).to eq(article.body_as_text)
  319. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  320. expect(generate['message']).to include('Updated by')
  321. end
  322. end
  323. end
  324. context "when the event is of the type 'info'" do
  325. let(:event) do
  326. {
  327. type: 'info',
  328. execution: 'trigger',
  329. changes: { 'state' => %w[open closed] },
  330. user_id: 1,
  331. }
  332. end
  333. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  334. expect(generate['subject']).to eq(ticket.title)
  335. expect(generate['body']).to eq(article.body_as_text)
  336. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  337. expect(generate['message']).to include('Last updated at')
  338. end
  339. end
  340. context "when the event is of the type 'escalation'" do
  341. let(:event) do
  342. {
  343. type: 'escalation',
  344. execution: 'trigger',
  345. user_id: 1,
  346. }
  347. end
  348. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  349. expect(generate['subject']).to eq(ticket.title)
  350. expect(generate['body']).to eq(article.body_as_text)
  351. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  352. expect(generate['message']).to include('Escalated at')
  353. expect(generate['changes']).to include('has been escalated since')
  354. end
  355. end
  356. context "when the event is of the type 'escalation warning'" do
  357. let(:event) do
  358. {
  359. type: 'escalation_warning',
  360. execution: 'trigger',
  361. user_id: 1,
  362. }
  363. end
  364. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  365. expect(generate['subject']).to eq(ticket.title)
  366. expect(generate['body']).to eq(article.body_as_text)
  367. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  368. expect(generate['message']).to include('Will escalate at')
  369. expect(generate['changes']).to include('will escalate at')
  370. end
  371. end
  372. context "when the event is of the type 'reminder reached'" do
  373. let(:event) do
  374. {
  375. type: 'reminder_reached',
  376. execution: 'trigger',
  377. user_id: 1,
  378. }
  379. end
  380. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  381. expect(generate['subject']).to eq(ticket.title)
  382. expect(generate['body']).to eq(article.body_as_text)
  383. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  384. expect(generate['message']).to include('Reminder reached!')
  385. expect(generate['changes']).to include('reminder reached for')
  386. end
  387. end
  388. context "when the event is triggered by a 'job'" do
  389. let(:event) do
  390. {
  391. type: '',
  392. execution: 'job',
  393. changes: { 'state' => %w[open closed] },
  394. user_id: 1,
  395. }
  396. end
  397. let(:article) { nil }
  398. it 'returns a valid json with a notification factory generated information"', :aggregate_failures do
  399. expect(generate['subject']).to eq(ticket.title)
  400. expect(generate['body']).to be_empty
  401. expect(generate['link']).to match(%r{http.*#ticket/zoom/#{ticket.id}$})
  402. expect(generate['message']).to include('Last updated at')
  403. end
  404. end
  405. end
  406. describe 'when the payload is a pre-defined webhook' do
  407. subject(:generate) { described_class.generate(record, { ticket:, article:, notification:, webhook: struct_webhook }) }
  408. let(:webhook) { create(:mattermost_webhook) }
  409. let(:struct_webhook) { TriggerWebhookJob::CustomPayload::Track::PreDefinedWebhook.generate({ ticket:, article: }, { event:, webhook: }) }
  410. let(:record) { TriggerWebhookJob::CustomPayload::Track::PreDefinedWebhook.payload('Mattermost') }
  411. it 'returns a valid json with webhook information"', :aggregate_failures do
  412. info = webhook.preferences[:pre_defined_webhook]
  413. expect(generate[:channel]).to eq(info[:channel])
  414. expect(generate[:icon_url]).to eq(info[:icon_url])
  415. end
  416. context 'when event has no changes' do
  417. let(:event) do
  418. {
  419. type: 'info',
  420. execution: 'trigger',
  421. changes: { 'state' => %w[open closed] },
  422. user_id: 1,
  423. }
  424. end
  425. it "returns a valid json with webhook information without 'attachments'", :aggregate_failures do
  426. info = webhook.preferences[:pre_defined_webhook]
  427. expect(generate[:channel]).to eq(info[:channel])
  428. expect(generate[:icon_url]).to eq(info[:icon_url])
  429. expect(generate).to not_include(:attachments)
  430. end
  431. end
  432. context 'when pre-defined webhook has no additional values' do
  433. let(:webhook) { create(:slack_webhook) }
  434. let(:record) { TriggerWebhookJob::CustomPayload::Track::PreDefinedWebhook.payload('Slack') }
  435. it 'returns a valid json with webhook information"', :aggregate_failures do
  436. expect(generate['text']).to eq("# #{ticket.title}")
  437. end
  438. end
  439. end
  440. end
  441. # rubocop:enable Lint/InterpolationCheck
  442. describe '.replacements' do
  443. subject(:replacements) { described_class.replacements(pre_defined_webhook_type: 'Mattermost') }
  444. it 'returns a hash with the replacement variables', :aggregate_failures do
  445. expect(replacements).to be_a(Hash)
  446. expect(replacements.keys).to include(:article, :ticket, :notification, :webhook)
  447. end
  448. end
  449. end