# Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' RSpec.describe NotificationFactory::Renderer do # rubocop:disable Lint/InterpolationCheck describe 'render' do before { @user = User.where(firstname: 'Nicole').first } it 'correctly renders a blank template' do renderer = build(:notification_factory_renderer) expect(renderer.render).to eq '' end context 'when rendering templates with ERB tags' do let(:template) { '<%% <%= "<%" %> %%>' } it 'ignores pre-existing ERB tags in an untrusted template' do renderer = build(:notification_factory_renderer, template: template) expect(renderer.render).to eq '<% <%= "<%" %> %%>' end it 'executes pre-existing ERB tags in a trusted template' do renderer = build(:notification_factory_renderer, template: template, trusted: true) expect(renderer.render).to eq '<% <% %%>' end end describe 'escaping' do let(:ticket) { create(:ticket, title: '< + some % special " characters') } let(:objects) { { ticket: ticket } } let(:renderer) { build(:notification_factory_renderer, objects: objects, template: template, escape: escape, url_encode: url_encode) } let(:escape) { false } let(:url_encode) { false } let(:template) { 'embedded #{ ticket.title } value' } context 'without escaping' do it 'renders correctly' do expect(renderer.render).to eq "embedded #{ticket.title} value" end end context 'with HTML escaping' do let(:escape) { true } it 'renders correctly' do expect(renderer.render).to eq 'embedded < + some % special " characters value' end end context 'with link encoding' do let(:url_encode) { true } it 'renders correctly' do expect(renderer.render).to eq 'embedded %3C%20%2B%20some%20%25%20special%20%22%20characters value' end end end describe 'interpolation error handling' do let(:renderer) { build(:notification_factory_renderer, objects: {}, template: template) } let(:template) { '#{ ticket.title }' } context 'with debug_errors' do it 'renders an debug message' do expect(renderer.render).to eq "\#{ticket / no such object}" end end context 'without debug_errors' do it 'renders a dash' do expect(renderer.render(debug_errors: false)).to eq '-' end end end it 'correctly renders chained object references' do user = User.where(firstname: 'Nicole').first ticket = create(:ticket, customer: user) renderer = build(:notification_factory_renderer, objects: { ticket: ticket }, template: '#{ticket.customer.firstname.downcase}') expect(renderer.render).to eq 'nicole' end it 'correctly renders multiple value calls' do ticket = create(:ticket, customer: @user) renderer = build(:notification_factory_renderer, objects: { ticket: ticket }, template: '#{ticket.created_at.value.value.value.value.to_s.first}') expect(renderer.render).to eq '2' end it 'raises a StandardError when rendering a template with a broken syntax' do renderer = build(:notification_factory_renderer, template: 'test <% if %>', objects: {}, trusted: true) expect { renderer.render }.to raise_error(StandardError) end it 'raises a StandardError when rendering a template calling a non existant method' do renderer = build(:notification_factory_renderer, template: 'test <% Ticket.non_existant_method %>', objects: {}, trusted: true) expect { renderer.render }.to raise_error(StandardError) end it 'raises a StandardError when rendering a template referencing a non existant object' do renderer = build(:notification_factory_renderer, template: 'test <% NonExistantObject.first %>', objects: {}, trusted: true) expect { renderer.render }.to raise_error(StandardError) end context 'with different article variables' do let(:customer) { create(:customer, firstname: 'Nicole') } let(:ticket) { create(:ticket, customer: customer) } let(:objects) do last_article = nil last_internal_article = nil last_external_article = nil all_articles = ticket.articles if article.nil? last_article = all_articles.last last_internal_article = all_articles.reverse.find(&:internal?) last_external_article = all_articles.reverse.find { |a| !a.internal? } else last_article = article last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?) last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article end { ticket: ticket, article: last_article, last_article: last_article, last_internal_article: last_internal_article, last_external_article: last_external_article, created_article: article, created_internal_article: article&.internal? ? article : nil, created_external_article: article&.internal? ? nil : article, } end let(:renderer) do build(:notification_factory_renderer, objects: objects, template: template) end let(:body) { 'test' } let(:article) { create(:ticket_article, ticket: ticket, body: body) } context 'with ticket.tags as template' do let(:template) { '#{ticket.tags}' } before do ticket.tag_add('Tag1', customer.id) end it 'correctly renders ticket tags references' do expect(renderer.render).to eq 'Tag1' end end %w[article last_article last_internal_article last_external_article created_article created_internal_article created_external_article].each do |tag| context "with #{tag}.body as template" do let(:template) { "\#{#{tag}.body}" } let(:article) do create( :ticket_article, ticket: ticket, body: body, internal: tag.match?('internal') ) end it "renders an #{tag} body with quote" do expect(renderer.render).to eq "> #{body}
" end context 'with links' do context 'with &' do let(:body) { "This is an example\nhttps://example.com/?query=foo&query2=bar" } it "renders an #{tag} body with working links" do expect(renderer.render).to eq '> This is an example
> https://example.com/?query=foo&query2=bar
' end end context 'with &' do let(:body) { "This is an example\nhttps://example.com/?query=foo&query2=bar" } it "renders an #{tag} body with working links" do expect(renderer.render).to eq '> This is an example
> https://example.com/?query=foo&query2=bar
' end end end end end end context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do before do create_object_manager_attribute ObjectManager::Attribute.migration_execute end let(:renderer) do build(:notification_factory_renderer, objects: { ticket: ticket }, template: template) end shared_examples 'correctly rendering the attributes' do it 'correctly renders the attributes' do expect(renderer.render).to eq expected_render end end context 'with a simple select attribute' do let(:create_object_manager_attribute) do create(:object_manager_attribute_select, name: 'select') end let(:ticket) { create(:ticket, customer: @user, select: 'key_1') } let(:template) { '#{ticket.select} _SEPERATOR_ #{ticket.select.value}' } let(:expected_render) { 'key_1 _SEPERATOR_ value_1' } it_behaves_like 'correctly rendering the attributes' end context 'with select attribute on chained user object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_select, object_lookup_id: ObjectLookup.by_name('User'), name: 'select') end let(:user) do user = User.where(firstname: 'Nicole').first user.select = 'key_2' user.save user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}' } let(:expected_render) { 'key_2 _SEPERATOR_ value_2' } it_behaves_like 'correctly rendering the attributes' end context 'with select attribute on chained group object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_select, object_lookup_id: ObjectLookup.by_name('Group'), name: 'select') end let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' } let(:expected_render) { 'key_3 _SEPERATOR_ value_3' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.select = 'key_3' group.save end it_behaves_like 'correctly rendering the attributes' end context 'with select attribute on chained organization object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_select, object_lookup_id: ObjectLookup.by_name('Organization'), name: 'select') end let(:user) do @user.organization.select = 'key_2' @user.organization.save @user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.organization.select} _SEPERATOR_ #{ticket.customer.organization.select.value}' } let(:expected_render) { 'key_2 _SEPERATOR_ value_2' } it_behaves_like 'correctly rendering the attributes' end context 'with multiselect', mariadb: true do context 'with a simple multiselect attribute' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, name: 'multiselect') end let(:ticket) { create(:ticket, customer: @user, multiselect: ['key_1']) } let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' } let(:expected_render) { 'key_1 _SEPERATOR_ value_1' } it_behaves_like 'correctly rendering the attributes' end context 'with single multiselect attribute on chained user object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('User'), name: 'multiselect') end let(:user) do user = User.where(firstname: 'Nicole').first user.multiselect = ['key_2'] user.save user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' } let(:expected_render) { 'key_2 _SEPERATOR_ value_2' } it_behaves_like 'correctly rendering the attributes' end context 'with single multiselect attribute on chained group object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('Group'), name: 'multiselect') end let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' } let(:expected_render) { 'key_3 _SEPERATOR_ value_3' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.multiselect = ['key_3'] group.save end it_behaves_like 'correctly rendering the attributes' end context 'with single multiselect attribute on chained organization object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('Organization'), name: 'multiselect') end let(:user) do @user.organization.multiselect = ['key_2'] @user.organization.save @user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' } let(:expected_render) { 'key_2 _SEPERATOR_ value_2' } it_behaves_like 'correctly rendering the attributes' end context 'with a multiple multiselect attribute' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, name: 'multiselect') end let(:ticket) { create(:ticket, customer: @user, multiselect: %w[key_1 key_2]) } let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' } let(:expected_render) { 'key_1, key_2 _SEPERATOR_ value_1, value_2' } it_behaves_like 'correctly rendering the attributes' end context 'with multiple multiselect attribute on chained user object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('User'), name: 'multiselect') end let(:user) do user = User.where(firstname: 'Nicole').first user.multiselect = %w[key_2 key_3] user.save user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' } let(:expected_render) { 'key_2, key_3 _SEPERATOR_ value_2, value_3' } it_behaves_like 'correctly rendering the attributes' end context 'with multiple multiselect attribute on chained group object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('Group'), name: 'multiselect') end let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' } let(:expected_render) { 'key_3, key_1 _SEPERATOR_ value_3, value_1' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.multiselect = %w[key_3 key_1] group.save end it_behaves_like 'correctly rendering the attributes' end context 'with select (custom sorted) attribute on chained group object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_select, object_lookup_id: ObjectLookup.by_name('Group'), name: 'select', data_option_options: [{ name: 'value_1', value: 'key_1' }, { name: 'value_2', value: 'key_2' }, { name: 'value_3', value: 'key_3' }]) end let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' } let(:expected_render) { 'key_3 _SEPERATOR_ value_3' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.select = 'key_3' group.save end it_behaves_like 'correctly rendering the attributes' end context 'with multiple multiselect (custom sorted) attribute on chained group object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('Group'), name: 'multiselect', data_option_options: [{ name: 'value_1', value: 'key_1' }, { name: 'value_2', value: 'key_2' }, { name: 'value_3', value: 'key_3' }]) end let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' } let(:expected_render) { 'key_3, key_1 _SEPERATOR_ value_3, value_1' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.multiselect = %w[key_3 key_1] group.save end it_behaves_like 'correctly rendering the attributes' end context 'with multiple multiselect attribute on chained organization object' do let(:create_object_manager_attribute) do create(:object_manager_attribute_multiselect, object_lookup_id: ObjectLookup.by_name('Organization'), name: 'multiselect') end let(:user) do @user.organization.multiselect = %w[key_2 key_1] @user.organization.save @user end let(:ticket) { create(:ticket, customer: user) } let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' } let(:expected_render) { 'key_2, key_1 _SEPERATOR_ value_2, value_1' } it_behaves_like 'correctly rendering the attributes' end context 'with external data source attribute on chained group object', db_adapter: :postgresql do let(:create_object_manager_attribute) do create(:object_manager_attribute_autocompletion_ajax_external_data_source, object_lookup_id: ObjectLookup.by_name('Group'), name: 'external_data_source') end let(:template) { '#{ticket.group.external_data_source} _SEPERATOR_ #{ticket.group.external_data_source.value}' } let(:expected_render) { '1234 _SEPERATOR_ Example' } let(:ticket) { create(:ticket, customer: @user) } before do group = ticket.group group.external_data_source = { value: 1234, label: 'Example' } group.save end it_behaves_like 'correctly rendering the attributes' end end context 'with a tree select attribute' do let(:create_object_manager_attribute) do create(:object_manager_attribute_tree_select, name: 'tree_select') end let(:ticket) { create(:ticket, customer: @user, tree_select: 'Incident::Hardware::Laptop') } let(:template) { '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}' } let(:expected_render) { 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop' } it_behaves_like 'correctly rendering the attributes' end context 'with a textarea attribute' do let(:create_object_manager_attribute) do create(:object_manager_attribute_textarea, name: 'textarea') create(:object_manager_attribute_textarea, name: 'textarea_empty') end let(:ticket) { create(:ticket, customer: @user, textarea: "Line 1\nLine 2\nLine 3", textarea_empty: nil) } let(:template) { '#{ticket.textarea} _SEPERATOR_ #{ticket.textarea.value} _SEPERATOR_ #{ticket.textarea_empty} _SEPERATOR_ #{ticket.textarea_empty.value}' } let(:expected_render) { 'Line 1
Line 2
Line 3 _SEPERATOR_ Line 1
Line 2
Line 3 _SEPERATOR_ _SEPERATOR_ ' } it_behaves_like 'correctly rendering the attributes' end end end # rubocop:enable Lint/InterpolationCheck context 'with user avatar' do let(:base64_img) { 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' } let(:decoded_img) { Base64.decode64(base64_img) } let(:mime_type) { 'image/png' } let(:avatar) do Avatar.add( object: 'User', o_id: owner.id, full: { content: decoded_img, mime_type: mime_type, }, resize: { content: decoded_img, mime_type: mime_type, }, source: "upload #{Time.zone.now}", deletable: true, created_by_id: owner.id, updated_by_id: owner.id, ) end let(:owner) { create(:user, group_ids: Group.pluck(:id)) } let(:ticket) { create(:ticket, owner: owner, group: Group.first) } context 'with an avatar' do before do owner.update!(image: avatar.store_hash) end it 'returns a tag' do renderer = build(:notification_factory_renderer, template: 'Avatar test #{ticket.owner.avatar(150, 150)}', objects: { ticket: ticket }, trusted: true) # rubocop:disable Lint/InterpolationCheck expect(renderer.render).to eq "Avatar test " end end context 'without an avatar' do it 'returns empty string' do renderer = build(:notification_factory_renderer, template: 'Avatar test #{ticket.owner.avatar(150, 150)}', objects: { ticket: ticket }, trusted: true) # rubocop:disable Lint/InterpolationCheck expect(renderer.render).to eq 'Avatar test ' end end end end