# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ require 'rails_helper' require 'models/application_model_examples' require 'models/concerns/has_xss_sanitized_note_examples' RSpec.describe Trigger, type: :model do subject(:trigger) { create(:trigger, condition: condition, perform: perform) } it_behaves_like 'ApplicationModel', can_assets: { selectors: %i[condition perform] } it_behaves_like 'HasXssSanitizedNote', model_factory: :trigger describe 'validation' do let(:condition) do { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } } end let(:perform) do { 'ticket.title' => { 'value'=>'triggered' } } end context 'notification.email' do context 'missing recipient' do let(:perform) do { 'notification.email' => { 'subject' => 'Hello', 'body' => 'World!' } } end it 'raises an error' do expect { trigger.save! }.to raise_error(Exceptions::UnprocessableEntity, 'Invalid perform notification.email, recipient is missing!') end end end end describe 'Send-email triggers' do before do described_class.destroy_all # Default DB state includes three sample triggers trigger # create subject trigger end let(:perform) do { 'notification.email' => { 'recipient' => 'ticket_customer', 'subject' => 'foo', 'body' => 'some body with >snip<#{article.body_as_html}>/snip<', # rubocop:disable Lint/InterpolationCheck } } end shared_examples 'include ticket attachment' do context 'notification.email include_attachments' do let(:perform) do { 'notification.email' => { 'recipient' => 'ticket_customer', 'subject' => 'Example subject', 'body' => 'Example body', } }.deep_merge(additional_options).deep_stringify_keys end let(:ticket) { create(:ticket) } shared_examples 'add a new article' do it 'adds a new article' do expect { TransactionDispatcher.commit } .to change(ticket.articles, :count).by(1) end end shared_examples 'add attachment to new article' do include_examples 'add a new article' it 'adds attachment to the new article' do ticket && trigger TransactionDispatcher.commit article = ticket.articles.last expect(article.type.name).to eq('email') expect(article.sender.name).to eq('System') expect(article.attachments.count).to eq(1) expect(article.attachments[0].filename).to eq('some_file.pdf') expect(article.attachments[0].preferences['Content-ID']).to eq('image/pdf@01CAB192.K8H512Y9') end end shared_examples 'does not add attachment to new article' do include_examples 'add a new article' it 'does not add attachment to the new article' do ticket && trigger TransactionDispatcher.commit article = ticket.articles.last expect(article.type.name).to eq('email') expect(article.sender.name).to eq('System') expect(article.attachments.count).to eq(0) end end context 'with include attachment present' do let(:additional_options) do { 'notification.email' => { include_attachments: 'true' } } end context 'when ticket has an attachment' do before do UserInfo.current_user_id = 1 ticket_article = create(:ticket_article, ticket: ticket) Store.add( object: 'Ticket::Article', o_id: ticket_article.id, data: 'dGVzdCAxMjM=', filename: 'some_file.pdf', preferences: { 'Content-Type': 'image/pdf', 'Content-ID': 'image/pdf@01CAB192.K8H512Y9', }, created_by_id: 1, ) end include_examples 'add attachment to new article' end context 'when ticket does not have an attachment' do include_examples 'does not add attachment to new article' end end context 'with include attachment not present' do let(:additional_options) do { 'notification.email' => { include_attachments: 'false' } } end context 'when ticket has an attachment' do before do UserInfo.current_user_id = 1 ticket_article = create(:ticket_article, ticket: ticket) Store.add( object: 'Ticket::Article', o_id: ticket_article.id, data: 'dGVzdCAxMjM=', filename: 'some_file.pdf', preferences: { 'Content-Type': 'image/pdf', 'Content-ID': 'image/pdf@01CAB192.K8H512Y9', }, created_by_id: 1, ) end include_examples 'does not add attachment to new article' end context 'when ticket does not have an attachment' do include_examples 'does not add attachment to new article' end end end end context 'for condition "ticket created"' do let(:condition) do { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } } end context 'when ticket is created directly' do let!(:ticket) { create(:ticket) } it 'fires (without altering ticket state)' do expect { TransactionDispatcher.commit } .to change(Ticket::Article, :count).by(1) .and not_change { ticket.reload.state.name }.from('new') end end context 'when ticket has tags' do let(:tag1) { create(:'tag/item', name: 't1') } let(:tag2) { create(:'tag/item', name: 't2') } let(:tag3) { create(:'tag/item', name: 't3') } let!(:ticket) do ticket = create(:ticket) create(:tag, o: ticket, tag_item: tag1) create(:tag, o: ticket, tag_item: tag2) create(:tag, o: ticket, tag_item: tag3) ticket end let(:perform) do { 'notification.email' => { 'recipient' => 'ticket_customer', 'subject' => 'foo', 'body' => 'some body with #{ticket.tags}', # rubocop:disable Lint/InterpolationCheck } } end it 'fires body with replaced tags' do TransactionDispatcher.commit expect(Ticket::Article.last.body).to eq('some body with t1, t2, t3') end end context 'when ticket is created via Channel::EmailParser.process' do before { create(:email_address, groups: [Group.first]) } let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail001.box')) } it 'fires (without altering ticket state)' do expect { Channel::EmailParser.new.process({}, raw_email) } .to change(Ticket, :count).by(1) .and change { Ticket::Article.count }.by(2) expect(Ticket.last.state.name).to eq('new') end end context 'when ticket is created via Channel::EmailParser.process with inline image' do before { create(:email_address, groups: [Group.first]) } let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail010.box')) } it 'fires (without altering ticket state)' do expect { Channel::EmailParser.new.process({}, raw_email) } .to change(Ticket, :count).by(1) .and change { Ticket::Article.count }.by(2) expect(Ticket.last.state.name).to eq('new') article = Ticket::Article.last expect(article.type.name).to eq('email') expect(article.sender.name).to eq('System') expect(article.attachments.count).to eq(1) expect(article.attachments[0].filename).to eq('image001.jpg') expect(article.attachments[0].preferences['Content-ID']).to eq('image001.jpg@01CDB132.D8A510F0') expect(article.body).to eq(<<~RAW.chomp some body with >snip<

Herzliche Grüße aus Oberalteich sendet Herrn Smith

 

Sepp Smith - Dipl.Ing. agr. (FH)

Geschäftsführer der example Straubing-Bogen

Klosterhof 1 | 94327 Bogen-Oberalteich

Tel: 09422-505601 | Fax: 09422-505620

Internet: http://example-straubing-bogen.de

Facebook: http://facebook.de/examplesrbog

Beschreibung: Beschreibung: efqmLogo - European Foundation für Quality Management

 

>/snip< RAW ) end end context 'notification.email recipient' do let!(:ticket) { create(:ticket) } let!(:recipient1) { create(:user, email: 'test1@zammad-test.com') } let!(:recipient2) { create(:user, email: 'test2@zammad-test.com') } let!(:recipient3) { create(:user, email: 'test3@zammad-test.com') } let(:perform) do { 'notification.email' => { 'recipient' => recipient, 'subject' => 'Hello', 'body' => 'World!' } } end before { TransactionDispatcher.commit } context 'mix of recipient group keyword and single recipient users' do let(:recipient) { [ 'ticket_customer', "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] } it 'contains all recipients' do expect(ticket.articles.last.to).to eq("#{ticket.customer.email}, #{recipient1.email}, #{recipient2.email}, #{recipient3.email}") end context 'duplicate recipient' do let(:recipient) { [ 'ticket_customer', "userid_#{ticket.customer.id}" ] } it 'contains only one recipient' do expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s) end end end context 'list of single users only' do let(:recipient) { [ "userid_#{recipient1.id}", "userid_#{recipient2.id}", "userid_#{recipient3.id}" ] } it 'contains all recipients' do expect(ticket.articles.last.to).to eq("#{recipient1.email}, #{recipient2.email}, #{recipient3.email}") end context 'assets' do it 'resolves Users from recipient list' do expect(trigger.assets({})[:User].keys).to include(recipient1.id, recipient2.id, recipient3.id) end context 'single entry' do let(:recipient) { "userid_#{recipient1.id}" } it 'resolves User from recipient list' do expect(trigger.assets({})[:User].keys).to include(recipient1.id) end end end end context 'recipient group keyword only' do let(:recipient) { 'ticket_customer' } it 'contains matching recipient' do expect(ticket.articles.last.to).to eq(ticket.customer.email.to_s) end end end context 'active S/MIME integration' do before do Setting.set('smime_integration', true) create(:smime_certificate, :with_private, fixture: system_email_address) create(:smime_certificate, fixture: customer_email_address) end let(:system_email_address) { 'smime1@example.com' } let(:customer_email_address) { 'smime2@example.com' } let(:email_address) { create(:email_address, email: system_email_address) } let(:group) { create(:group, email_address: email_address) } let(:customer) { create(:customer, email: customer_email_address) } let(:security_preferences) { Ticket::Article.last.preferences[:security] } let(:perform) do { 'notification.email' => { 'recipient' => 'ticket_customer', 'subject' => 'Subject dummy.', 'body' => 'Body dummy.', }.merge(security_configuration) } end let!(:ticket) { create(:ticket, group: group, customer: customer) } context 'sending articles' do before do TransactionDispatcher.commit end context 'expired certificate' do let(:system_email_address) { 'expiredsmime1@example.com' } let(:security_configuration) do { 'sign' => 'always', 'encryption' => 'always', } end it 'creates unsigned article' do expect(security_preferences[:sign][:success]).to be false expect(security_preferences[:encryption][:success]).to be true end end context 'sign and encryption not set' do let(:security_configuration) { {} } it 'does not sign or encrypt' do expect(security_preferences[:sign][:success]).to be false expect(security_preferences[:encryption][:success]).to be false end end context 'sign and encryption disabled' do let(:security_configuration) do { 'sign' => 'no', 'encryption' => 'no', } end it 'does not sign or encrypt' do expect(security_preferences[:sign][:success]).to be false expect(security_preferences[:encryption][:success]).to be false end end context 'sign is enabled' do let(:security_configuration) do { 'sign' => 'always', 'encryption' => 'no', } end it 'signs' do expect(security_preferences[:sign][:success]).to be true expect(security_preferences[:encryption][:success]).to be false end end context 'encryption enabled' do let(:security_configuration) do { 'sign' => 'no', 'encryption' => 'always', } end it 'encrypts' do expect(security_preferences[:sign][:success]).to be false expect(security_preferences[:encryption][:success]).to be true end end context 'sign and encryption enabled' do let(:security_configuration) do { 'sign' => 'always', 'encryption' => 'always', } end it 'signs and encrypts' do expect(security_preferences[:sign][:success]).to be true expect(security_preferences[:encryption][:success]).to be true end end end context 'discard' do context 'sign' do let(:security_configuration) do { 'sign' => 'discard', } end context 'group without certificate' do let(:group) { create(:group) } it 'does not fire' do expect { TransactionDispatcher.commit } .to change(Ticket::Article, :count).by(0) end end end context 'encryption' do let(:security_configuration) do { 'encryption' => 'discard', } end context 'customer without certificate' do let(:customer) { create(:customer) } it 'does not fire' do expect { TransactionDispatcher.commit } .to change(Ticket::Article, :count).by(0) end end end context 'mixed' do context 'sign' do let(:security_configuration) do { 'encryption' => 'always', 'sign' => 'discard', } end context 'group without certificate' do let(:group) { create(:group) } it 'does not fire' do expect { TransactionDispatcher.commit } .to change(Ticket::Article, :count).by(0) end end end context 'encryption' do let(:security_configuration) do { 'encryption' => 'discard', 'sign' => 'always', } end context 'customer without certificate' do let(:customer) { create(:customer) } it 'does not fire' do expect { TransactionDispatcher.commit } .to change(Ticket::Article, :count).by(0) end end end end end end include_examples 'include ticket attachment' end context 'for condition "ticket updated"' do let(:condition) do { 'ticket.action' => { 'operator' => 'is', 'value' => 'update' } } end let!(:ticket) { create(:ticket).tap { TransactionDispatcher.commit } } context 'when new article is created directly' do context 'with empty #preferences hash' do let!(:article) { create(:ticket_article, ticket: ticket) } it 'fires (without altering ticket state)' do expect { TransactionDispatcher.commit } .to change { ticket.reload.articles.count }.by(1) .and not_change { ticket.reload.state.name }.from('new') end end context 'with #preferences { "send-auto-response" => false }' do let!(:article) do create(:ticket_article, ticket: ticket, preferences: { 'send-auto-response' => false }) end it 'does not fire' do expect { TransactionDispatcher.commit } .not_to change { ticket.reload.articles.count } end end end context 'when new article is created via Channel::EmailParser.process' do context 'with a regular message' do let!(:article) do create(:ticket_article, ticket: ticket, message_id: raw_email[%r{(?<=^References: )\S*}], subject: raw_email[%r{(?<=^Subject: Re: ).*$}]) end let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail005.box')) } it 'fires (without altering ticket state)' do expect { Channel::EmailParser.new.process({}, raw_email) } .to not_change { Ticket.count } .and change { ticket.reload.articles.count }.by(2) .and not_change { ticket.reload.state.name }.from('new') end end context 'with delivery-failed "bounce message"' do let!(:article) do create(:ticket_article, ticket: ticket, message_id: raw_email[%r{(?<=^Message-ID: )\S*}]) end let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail055.box')) } it 'does not fire' do expect { Channel::EmailParser.new.process({}, raw_email) } .to change { ticket.reload.articles.count }.by(1) end end end end context 'with condition execution_time.calendar_id' do let(:calendar) { create(:calendar) } let(:perform) do { 'ticket.title'=>{ 'value'=>'triggered' } } end let!(:ticket) { create(:ticket, title: 'Test Ticket') } context 'is in working time' do let(:condition) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) }, 'execution_time.calendar_id' => { 'operator' => 'is in working time', 'value' => calendar.id } } end it 'does trigger only in working time' do travel_to Time.zone.parse('2020-02-12T12:00:00Z0') expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered') end it 'does not trigger out of working time' do travel_to Time.zone.parse('2020-02-12T02:00:00Z0') TransactionDispatcher.commit expect(ticket.reload.title).to eq('Test Ticket') end end context 'is not in working time' do let(:condition) do { 'execution_time.calendar_id' => { 'operator' => 'is not in working time', 'value' => calendar.id } } end it 'does not trigger in working time' do travel_to Time.zone.parse('2020-02-12T12:00:00Z0') TransactionDispatcher.commit expect(ticket.reload.title).to eq('Test Ticket') end it 'does trigger out of working time' do travel_to Time.zone.parse('2020-02-12T02:00:00Z0') expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered') end end end context 'with article last sender equals system address' do let!(:ticket) { create(:ticket) } let(:perform) do { 'notification.email' => { 'recipient' => 'article_last_sender', 'subject' => 'foo last sender', 'body' => 'some body with >snip<#{article.body_as_html}>/snip<', # rubocop:disable Lint/InterpolationCheck } } end let(:condition) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.all.pluck(:id) } } end let!(:system_address) do create(:email_address) end context 'article with from equal to the a system address' do let!(:article) do create(:ticket_article, ticket: ticket, from: system_address.email,) end it 'does not trigger because of the last article is created my system address' do expect { TransactionDispatcher.commit }.to change { ticket.reload.articles.count }.by(0) expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender') expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email) end end context 'article with reply_to equal to the a system address' do let!(:article) do create(:ticket_article, ticket: ticket, from: system_address.email, reply_to: system_address.email,) end it 'does not trigger because of the last article is created my system address' do expect { TransactionDispatcher.commit }.to change { ticket.reload.articles.count }.by(0) expect(Ticket::Article.where(ticket: ticket).last.subject).not_to eq('foo last sender') expect(Ticket::Article.where(ticket: ticket).last.to).not_to eq(system_address.email) end end include_examples 'include ticket attachment' end end context 'with pre condition current_user.id' do let(:perform) do { 'ticket.title'=>{ 'value'=>'triggered' } } end let(:user) do user = create(:agent) user.roles.first.groups << group user end let(:group) { Group.first } let(:ticket) do create(:ticket, title: 'Test Ticket', group: group, owner_id: user.id, created_by_id: user.id, updated_by_id: user.id) end shared_examples 'successful trigger' do |attribute:| let(:attribute) { attribute } let(:condition) do { attribute => { operator: 'is', pre_condition: 'current_user.id', value: '', value_completion: '' } } end it "for #{attribute}" do ticket && trigger expect { TransactionDispatcher.commit }.to change { ticket.reload.title }.to('triggered') end end it_behaves_like 'successful trigger', attribute: 'ticket.updated_by_id' it_behaves_like 'successful trigger', attribute: 'ticket.owner_id' end describe 'Multi-trigger interactions:' do let(:ticket) { create(:ticket) } context 'cascading (i.e., trigger A satisfies trigger B satisfies trigger C)' do subject!(:triggers) do [ create(:trigger, condition: initial_state, perform: first_change, name: 'A'), create(:trigger, condition: first_change, perform: second_change, name: 'B'), create(:trigger, condition: second_change, perform: third_change, name: 'C') ] end context 'in a chain' do let(:initial_state) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'new').id.to_s, } } end let(:first_change) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'open').id.to_s, } } end let(:second_change) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'closed').id.to_s, } } end let(:third_change) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'merged').id.to_s, } } end context 'in alphabetical order (by name)' do it 'fires all triggers in sequence' do expect { TransactionDispatcher.commit } .to change { ticket.reload.state.name }.to('merged') end end context 'out of alphabetical order (by name)' do before do triggers.first.update(name: 'E') triggers.second.update(name: 'F') triggers.third.update(name: 'D') end context 'with Setting ticket_trigger_recursive: true' do before { Setting.set('ticket_trigger_recursive', true) } it 'evaluates triggers in sequence, then loops back to the start and re-evalutes skipped triggers' do expect { TransactionDispatcher.commit } .to change { ticket.reload.state.name }.to('merged') end end context 'with Setting ticket_trigger_recursive: false' do before { Setting.set('ticket_trigger_recursive', false) } it 'evaluates triggers in sequence, firing only the ones that match' do expect { TransactionDispatcher.commit } .to change { ticket.reload.state.name }.to('closed') end end end end context 'in circular reference (i.e., trigger A satisfies trigger B satisfies trigger C satisfies trigger A...)' do let(:initial_state) do { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s, } } end let(:first_change) do { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s, } } end let(:second_change) do { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.lookup(name: '1 low').id.to_s, } } end let(:third_change) do { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.lookup(name: '2 normal').id.to_s, } } end context 'with Setting ticket_trigger_recursive: true' do before { Setting.set('ticket_trigger_recursive', true) } it 'fires each trigger once, without being caught in an endless loop' do expect { Timeout.timeout(2) { TransactionDispatcher.commit } } .to not_change { ticket.reload.priority.name } .and not_raise_error end end context 'with Setting ticket_trigger_recursive: false' do before { Setting.set('ticket_trigger_recursive', false) } it 'fires each trigger once, without being caught in an endless loop' do expect { Timeout.timeout(2) { TransactionDispatcher.commit } } .to not_change { ticket.reload.priority.name } .and not_raise_error end end end end context 'competing (i.e., trigger A un-satisfies trigger B)' do subject!(:triggers) do [ create(:trigger, condition: initial_state, perform: change_a, name: 'A'), create(:trigger, condition: initial_state, perform: change_b, name: 'B') ] end let(:initial_state) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'new').id.to_s, } } end let(:change_a) do { 'ticket.state_id' => { 'operator' => 'is', 'value' => Ticket::State.lookup(name: 'open').id.to_s, } } end let(:change_b) do { 'ticket.priority_id' => { 'operator' => 'is', 'value' => Ticket::Priority.lookup(name: '3 high').id.to_s, } } end it 'evaluates triggers in sequence, firing only the ones that match' do expect { TransactionDispatcher.commit } .to change { ticket.reload.state.name }.to('open') .and not_change { ticket.reload.priority.name } end end end end