renderer_spec.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe NotificationFactory::Renderer do
  4. # rubocop:disable Lint/InterpolationCheck
  5. describe 'render' do
  6. before { @user = User.where(firstname: 'Nicole').first }
  7. it 'correctly renders a blank template' do
  8. renderer = build(:notification_factory_renderer)
  9. expect(renderer.render).to eq ''
  10. end
  11. context 'when rendering templates with ERB tags' do
  12. let(:template) { '<%% <%= "<%" %> %%>' }
  13. it 'ignores pre-existing ERB tags in an untrusted template' do
  14. renderer = build(:notification_factory_renderer, template: template)
  15. expect(renderer.render).to eq '<% <%= "<%" %> %%>'
  16. end
  17. it 'executes pre-existing ERB tags in a trusted template' do
  18. renderer = build(:notification_factory_renderer, template: template, trusted: true)
  19. expect(renderer.render).to eq '<% <% %%>'
  20. end
  21. end
  22. describe 'escaping' do
  23. let(:ticket) { create(:ticket, title: '< + some % special " characters') }
  24. let(:objects) { { ticket: ticket } }
  25. let(:renderer) { build(:notification_factory_renderer, objects: objects, template: template, escape: escape, url_encode: url_encode) }
  26. let(:escape) { false }
  27. let(:url_encode) { false }
  28. let(:template) { 'embedded #{ ticket.title } value' }
  29. context 'without escaping' do
  30. it 'renders correctly' do
  31. expect(renderer.render).to eq "embedded #{ticket.title} value"
  32. end
  33. end
  34. context 'with HTML escaping' do
  35. let(:escape) { true }
  36. it 'renders correctly' do
  37. expect(renderer.render).to eq 'embedded &lt; + some % special &quot; characters value'
  38. end
  39. end
  40. context 'with link encoding' do
  41. let(:url_encode) { true }
  42. it 'renders correctly' do
  43. expect(renderer.render).to eq 'embedded %3C%20%2B%20some%20%25%20special%20%22%20characters value'
  44. end
  45. end
  46. end
  47. describe 'interpolation error handling' do
  48. let(:renderer) { build(:notification_factory_renderer, objects: {}, template: template) }
  49. let(:template) { '#{ ticket.title }' }
  50. context 'with debug_errors' do
  51. it 'renders an debug message' do
  52. expect(renderer.render).to eq "\#{ticket / no such object}"
  53. end
  54. end
  55. context 'without debug_errors' do
  56. it 'renders a dash' do
  57. expect(renderer.render(debug_errors: false)).to eq '-'
  58. end
  59. end
  60. end
  61. it 'correctly renders chained object references' do
  62. user = User.where(firstname: 'Nicole').first
  63. ticket = create(:ticket, customer: user)
  64. renderer = build(:notification_factory_renderer,
  65. objects: { ticket: ticket },
  66. template: '#{ticket.customer.firstname.downcase}')
  67. expect(renderer.render).to eq 'nicole'
  68. end
  69. it 'correctly renders multiple value calls' do
  70. ticket = create(:ticket, customer: @user)
  71. renderer = build(:notification_factory_renderer,
  72. objects: { ticket: ticket },
  73. template: '#{ticket.created_at.value.value.value.value.to_s.first}')
  74. expect(renderer.render).to eq '2'
  75. end
  76. it 'raises a StandardError when rendering a template with a broken syntax' do
  77. renderer = build(:notification_factory_renderer, template: 'test <% if %>', objects: {}, trusted: true)
  78. expect { renderer.render }.to raise_error(StandardError)
  79. end
  80. it 'raises a StandardError when rendering a template calling a non existant method' do
  81. renderer = build(:notification_factory_renderer, template: 'test <% Ticket.non_existant_method %>', objects: {}, trusted: true)
  82. expect { renderer.render }.to raise_error(StandardError)
  83. end
  84. it 'raises a StandardError when rendering a template referencing a non existant object' do
  85. renderer = build(:notification_factory_renderer, template: 'test <% NonExistantObject.first %>', objects: {}, trusted: true)
  86. expect { renderer.render }.to raise_error(StandardError)
  87. end
  88. context 'with different article variables' do
  89. let(:customer) { create(:customer, firstname: 'Nicole') }
  90. let(:ticket) { create(:ticket, customer: customer) }
  91. let(:objects) do
  92. last_article = nil
  93. last_internal_article = nil
  94. last_external_article = nil
  95. all_articles = ticket.articles
  96. if article.nil?
  97. last_article = all_articles.last
  98. last_internal_article = all_articles.reverse.find(&:internal?)
  99. last_external_article = all_articles.reverse.find { |a| !a.internal? }
  100. else
  101. last_article = article
  102. last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?)
  103. last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article
  104. end
  105. {
  106. ticket: ticket,
  107. article: last_article,
  108. last_article: last_article,
  109. last_internal_article: last_internal_article,
  110. last_external_article: last_external_article,
  111. created_article: article,
  112. created_internal_article: article&.internal? ? article : nil,
  113. created_external_article: article&.internal? ? nil : article,
  114. }
  115. end
  116. let(:renderer) do
  117. build(:notification_factory_renderer,
  118. objects: objects,
  119. template: template)
  120. end
  121. let(:body) { 'test' }
  122. let(:article) { create(:ticket_article, ticket: ticket, body: body) }
  123. context 'with ticket.tags as template' do
  124. let(:template) { '#{ticket.tags}' }
  125. before do
  126. ticket.tag_add('Tag1', customer.id)
  127. end
  128. it 'correctly renders ticket tags references' do
  129. expect(renderer.render).to eq 'Tag1'
  130. end
  131. end
  132. %w[article last_article last_internal_article last_external_article
  133. created_article created_internal_article created_external_article].each do |tag|
  134. context "with #{tag}.body as template" do
  135. let(:template) { "\#{#{tag}.body}" }
  136. let(:article) do
  137. create(
  138. :ticket_article,
  139. ticket: ticket,
  140. body: body,
  141. internal: tag.match?('internal')
  142. )
  143. end
  144. it "renders an #{tag} body with quote" do
  145. expect(renderer.render).to eq "&gt; #{body}<br>"
  146. end
  147. context 'with links' do
  148. context 'with &amp;' do
  149. let(:body) { "This is an example\nhttps://example.com/?query=foo&amp;query2=bar" }
  150. it "renders an #{tag} body with working links" do
  151. expect(renderer.render).to eq '&gt; This is an example<br>&gt; https://example.com/?query=foo&amp;query2=bar<br>'
  152. end
  153. end
  154. context 'with &' do
  155. let(:body) { "This is an example\nhttps://example.com/?query=foo&query2=bar" }
  156. it "renders an #{tag} body with working links" do
  157. expect(renderer.render).to eq '&gt; This is an example<br>&gt; https://example.com/?query=foo&amp;query2=bar<br>'
  158. end
  159. end
  160. end
  161. end
  162. end
  163. end
  164. context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do
  165. before do
  166. create_object_manager_attribute
  167. ObjectManager::Attribute.migration_execute
  168. end
  169. let(:renderer) do
  170. build(:notification_factory_renderer,
  171. objects: { ticket: ticket },
  172. template: template)
  173. end
  174. shared_examples 'correctly rendering the attributes' do
  175. it 'correctly renders the attributes' do
  176. expect(renderer.render).to eq expected_render
  177. end
  178. end
  179. context 'with a simple select attribute' do
  180. let(:create_object_manager_attribute) do
  181. create(:object_manager_attribute_select, name: 'select')
  182. end
  183. let(:ticket) { create(:ticket, customer: @user, select: 'key_1') }
  184. let(:template) { '#{ticket.select} _SEPERATOR_ #{ticket.select.value}' }
  185. let(:expected_render) { 'key_1 _SEPERATOR_ value_1' }
  186. it_behaves_like 'correctly rendering the attributes'
  187. end
  188. context 'with select attribute on chained user object' do
  189. let(:create_object_manager_attribute) do
  190. create(:object_manager_attribute_select,
  191. object_lookup_id: ObjectLookup.by_name('User'),
  192. name: 'select')
  193. end
  194. let(:user) do
  195. user = User.where(firstname: 'Nicole').first
  196. user.select = 'key_2'
  197. user.save
  198. user
  199. end
  200. let(:ticket) { create(:ticket, customer: user) }
  201. let(:template) { '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}' }
  202. let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
  203. it_behaves_like 'correctly rendering the attributes'
  204. end
  205. context 'with select attribute on chained group object' do
  206. let(:create_object_manager_attribute) do
  207. create(:object_manager_attribute_select,
  208. object_lookup_id: ObjectLookup.by_name('Group'),
  209. name: 'select')
  210. end
  211. let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' }
  212. let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
  213. let(:ticket) { create(:ticket, customer: @user) }
  214. before do
  215. group = ticket.group
  216. group.select = 'key_3'
  217. group.save
  218. end
  219. it_behaves_like 'correctly rendering the attributes'
  220. end
  221. context 'with select attribute on chained organization object' do
  222. let(:create_object_manager_attribute) do
  223. create(:object_manager_attribute_select,
  224. object_lookup_id: ObjectLookup.by_name('Organization'),
  225. name: 'select')
  226. end
  227. let(:user) do
  228. @user.organization.select = 'key_2'
  229. @user.organization.save
  230. @user
  231. end
  232. let(:ticket) { create(:ticket, customer: user) }
  233. let(:template) { '#{ticket.customer.organization.select} _SEPERATOR_ #{ticket.customer.organization.select.value}' }
  234. let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
  235. it_behaves_like 'correctly rendering the attributes'
  236. end
  237. context 'with multiselect', mariadb: true do
  238. context 'with a simple multiselect attribute' do
  239. let(:create_object_manager_attribute) do
  240. create(:object_manager_attribute_multiselect, name: 'multiselect')
  241. end
  242. let(:ticket) { create(:ticket, customer: @user, multiselect: ['key_1']) }
  243. let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' }
  244. let(:expected_render) { 'key_1 _SEPERATOR_ value_1' }
  245. it_behaves_like 'correctly rendering the attributes'
  246. end
  247. context 'with single multiselect attribute on chained user object' do
  248. let(:create_object_manager_attribute) do
  249. create(:object_manager_attribute_multiselect,
  250. object_lookup_id: ObjectLookup.by_name('User'),
  251. name: 'multiselect')
  252. end
  253. let(:user) do
  254. user = User.where(firstname: 'Nicole').first
  255. user.multiselect = ['key_2']
  256. user.save
  257. user
  258. end
  259. let(:ticket) { create(:ticket, customer: user) }
  260. let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' }
  261. let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
  262. it_behaves_like 'correctly rendering the attributes'
  263. end
  264. context 'with single multiselect attribute on chained group object' do
  265. let(:create_object_manager_attribute) do
  266. create(:object_manager_attribute_multiselect,
  267. object_lookup_id: ObjectLookup.by_name('Group'),
  268. name: 'multiselect')
  269. end
  270. let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' }
  271. let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
  272. let(:ticket) { create(:ticket, customer: @user) }
  273. before do
  274. group = ticket.group
  275. group.multiselect = ['key_3']
  276. group.save
  277. end
  278. it_behaves_like 'correctly rendering the attributes'
  279. end
  280. context 'with single multiselect attribute on chained organization object' do
  281. let(:create_object_manager_attribute) do
  282. create(:object_manager_attribute_multiselect,
  283. object_lookup_id: ObjectLookup.by_name('Organization'),
  284. name: 'multiselect')
  285. end
  286. let(:user) do
  287. @user.organization.multiselect = ['key_2']
  288. @user.organization.save
  289. @user
  290. end
  291. let(:ticket) { create(:ticket, customer: user) }
  292. let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' }
  293. let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
  294. it_behaves_like 'correctly rendering the attributes'
  295. end
  296. context 'with a multiple multiselect attribute' do
  297. let(:create_object_manager_attribute) do
  298. create(:object_manager_attribute_multiselect, name: 'multiselect')
  299. end
  300. let(:ticket) { create(:ticket, customer: @user, multiselect: %w[key_1 key_2]) }
  301. let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' }
  302. let(:expected_render) { 'key_1, key_2 _SEPERATOR_ value_1, value_2' }
  303. it_behaves_like 'correctly rendering the attributes'
  304. end
  305. context 'with multiple multiselect attribute on chained user object' do
  306. let(:create_object_manager_attribute) do
  307. create(:object_manager_attribute_multiselect,
  308. object_lookup_id: ObjectLookup.by_name('User'),
  309. name: 'multiselect')
  310. end
  311. let(:user) do
  312. user = User.where(firstname: 'Nicole').first
  313. user.multiselect = %w[key_2 key_3]
  314. user.save
  315. user
  316. end
  317. let(:ticket) { create(:ticket, customer: user) }
  318. let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' }
  319. let(:expected_render) { 'key_2, key_3 _SEPERATOR_ value_2, value_3' }
  320. it_behaves_like 'correctly rendering the attributes'
  321. end
  322. context 'with multiple multiselect attribute on chained group object' do
  323. let(:create_object_manager_attribute) do
  324. create(:object_manager_attribute_multiselect,
  325. object_lookup_id: ObjectLookup.by_name('Group'),
  326. name: 'multiselect')
  327. end
  328. let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' }
  329. let(:expected_render) { 'key_3, key_1 _SEPERATOR_ value_3, value_1' }
  330. let(:ticket) { create(:ticket, customer: @user) }
  331. before do
  332. group = ticket.group
  333. group.multiselect = %w[key_3 key_1]
  334. group.save
  335. end
  336. it_behaves_like 'correctly rendering the attributes'
  337. end
  338. context 'with select (custom sorted) attribute on chained group object' do
  339. let(:create_object_manager_attribute) do
  340. create(:object_manager_attribute_select,
  341. object_lookup_id: ObjectLookup.by_name('Group'),
  342. name: 'select',
  343. data_option_options: [{ name: 'value_1', value: 'key_1' }, { name: 'value_2', value: 'key_2' }, { name: 'value_3', value: 'key_3' }])
  344. end
  345. let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' }
  346. let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
  347. let(:ticket) { create(:ticket, customer: @user) }
  348. before do
  349. group = ticket.group
  350. group.select = 'key_3'
  351. group.save
  352. end
  353. it_behaves_like 'correctly rendering the attributes'
  354. end
  355. context 'with multiple multiselect (custom sorted) attribute on chained group object' do
  356. let(:create_object_manager_attribute) do
  357. create(:object_manager_attribute_multiselect,
  358. object_lookup_id: ObjectLookup.by_name('Group'),
  359. name: 'multiselect',
  360. data_option_options: [{ name: 'value_1', value: 'key_1' }, { name: 'value_2', value: 'key_2' }, { name: 'value_3', value: 'key_3' }])
  361. end
  362. let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' }
  363. let(:expected_render) { 'key_3, key_1 _SEPERATOR_ value_3, value_1' }
  364. let(:ticket) { create(:ticket, customer: @user) }
  365. before do
  366. group = ticket.group
  367. group.multiselect = %w[key_3 key_1]
  368. group.save
  369. end
  370. it_behaves_like 'correctly rendering the attributes'
  371. end
  372. context 'with multiple multiselect attribute on chained organization object' do
  373. let(:create_object_manager_attribute) do
  374. create(:object_manager_attribute_multiselect,
  375. object_lookup_id: ObjectLookup.by_name('Organization'),
  376. name: 'multiselect')
  377. end
  378. let(:user) do
  379. @user.organization.multiselect = %w[key_2 key_1]
  380. @user.organization.save
  381. @user
  382. end
  383. let(:ticket) { create(:ticket, customer: user) }
  384. let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' }
  385. let(:expected_render) { 'key_2, key_1 _SEPERATOR_ value_2, value_1' }
  386. it_behaves_like 'correctly rendering the attributes'
  387. end
  388. context 'with external data source attribute on chained group object', db_adapter: :postgresql do
  389. let(:create_object_manager_attribute) do
  390. create(:object_manager_attribute_autocompletion_ajax_external_data_source,
  391. object_lookup_id: ObjectLookup.by_name('Group'),
  392. name: 'external_data_source')
  393. end
  394. let(:template) { '#{ticket.group.external_data_source} _SEPERATOR_ #{ticket.group.external_data_source.value}' }
  395. let(:expected_render) { '1234 _SEPERATOR_ Example' }
  396. let(:ticket) { create(:ticket, customer: @user) }
  397. before do
  398. group = ticket.group
  399. group.external_data_source = {
  400. value: 1234,
  401. label: 'Example'
  402. }
  403. group.save
  404. end
  405. it_behaves_like 'correctly rendering the attributes'
  406. end
  407. end
  408. context 'with a tree select attribute' do
  409. let(:create_object_manager_attribute) do
  410. create(:object_manager_attribute_tree_select, name: 'tree_select')
  411. end
  412. let(:ticket) { create(:ticket, customer: @user, tree_select: 'Incident::Hardware::Laptop') }
  413. let(:template) { '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}' }
  414. let(:expected_render) { 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop' }
  415. it_behaves_like 'correctly rendering the attributes'
  416. end
  417. end
  418. end
  419. # rubocop:enable Lint/InterpolationCheck
  420. context 'with user avatar' do
  421. let(:base64_img) { 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' }
  422. let(:decoded_img) { Base64.decode64(base64_img) }
  423. let(:mime_type) { 'image/png' }
  424. let(:avatar) do
  425. Avatar.add(
  426. object: 'User',
  427. o_id: owner.id,
  428. full: {
  429. content: decoded_img,
  430. mime_type: mime_type,
  431. },
  432. resize: {
  433. content: decoded_img,
  434. mime_type: mime_type,
  435. },
  436. source: "upload #{Time.zone.now}",
  437. deletable: true,
  438. created_by_id: owner.id,
  439. updated_by_id: owner.id,
  440. )
  441. end
  442. let(:owner) { create(:user, group_ids: Group.pluck(:id)) }
  443. let(:ticket) { create(:ticket, owner: owner, group: Group.first) }
  444. context 'with an avatar' do
  445. before do
  446. owner.update!(image: avatar.store_hash)
  447. end
  448. it 'returns a <img> tag' do
  449. renderer = build(:notification_factory_renderer, template: 'Avatar test #{ticket.owner.avatar(150, 150)}', objects: { ticket: ticket }, trusted: true) # rubocop:disable Lint/InterpolationCheck
  450. expect(renderer.render).to eq "Avatar test <img src='data:#{mime_type};base64,#{base64_img}' width='150' height='150' />"
  451. end
  452. end
  453. context 'without an avatar' do
  454. it 'returns empty string' do
  455. renderer = build(:notification_factory_renderer, template: 'Avatar test #{ticket.owner.avatar(150, 150)}', objects: { ticket: ticket }, trusted: true) # rubocop:disable Lint/InterpolationCheck
  456. expect(renderer.render).to eq 'Avatar test '
  457. end
  458. end
  459. end
  460. end