custom_payload_spec.rb 19 KB

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