1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150 |
- # 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<<div>
- <p>Herzliche Grüße aus Oberalteich sendet Herrn Smith</p>
- <p> </p>
- <p>Sepp Smith - Dipl.Ing. agr. (FH)</p>
- <p>Geschäftsführer der example Straubing-Bogen</p>
- <p>Klosterhof 1 | 94327 Bogen-Oberalteich</p>
- <p>Tel: 09422-505601 | Fax: 09422-505620</p>
- <p><span>Internet: <a href="http://example-straubing-bogen.de/" rel="nofollow noreferrer noopener" target="_blank"><span style="color:blue;">http://example-straubing-bogen.de</span></a></span></p>
- <p><span lang="EN-US">Facebook: </span><a href="http://facebook.de/examplesrbog" rel="nofollow noreferrer noopener" target="_blank"><span lang="EN-US" style="color:blue;">http://facebook.de/examplesrbog</span></a><span lang="EN-US"></span></p>
- <p><b><span style="color:navy;"><img border="0" src="cid:image001.jpg@01CDB132.D8A510F0" alt="Beschreibung: Beschreibung: efqmLogo" style="width:60px;height:19px;"></span></b><b><span lang="EN-US" style="color:navy;"> - European Foundation für Quality Management</span></b><span lang="EN-US"></span></p>
- <p><span lang="EN-US"><p> </p></span></p>
- </div>>/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
- describe 'multiselect triggers', db_strategy: :reset do
- let(:attribute_name) { 'multiselect' }
- let(:condition) do
- { "ticket.#{attribute_name}" => { 'operator' => operator, 'value' => trigger_values } }
- end
- let(:perform) do
- { 'article.note' => { 'subject' => 'Test subject note', 'internal' => 'true', 'body' => 'Test body note' } }
- end
- before do
- create :object_manager_attribute_multiselect, name: attribute_name
- ObjectManager::Attribute.migration_execute
- described_class.destroy_all # Default DB state includes three sample triggers
- trigger # create subject trigger
- end
- context 'when ticket is updated with a multiselect trigger condition', authenticated_as: :owner, db_strategy: :reset do
- let(:options) do
- {
- a: 'a',
- b: 'b',
- c: 'c',
- d: 'd',
- e: 'e',
- }
- end
- let(:trigger_values) { %w[a b c] }
- let(:group) { create(:group) }
- let(:owner) { create(:admin, group_ids: [group.id]) }
- let!(:ticket) { create(:ticket, group: group,) }
- before do
- ticket.update_attribute(attribute_name, ticket_multiselect_values)
- end
- shared_examples 'updating the ticket with the trigger condition' do
- it 'updates the ticket with the trigger condition' do
- expect { TransactionDispatcher.commit }
- .to change(Ticket::Article, :count).by(1)
- end
- end
- shared_examples 'not updating the ticket with the trigger condition' do
- it 'does not update the ticket with the trigger condition' do
- expect { TransactionDispatcher.commit }
- .to not_change(Ticket::Article, :count)
- end
- end
- context "with 'contains all' used" do
- let(:operator) { 'contains all' }
- context 'when updated value is the same with trigger value' do
- let(:ticket_multiselect_values) { trigger_values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value is different from the trigger value' do
- let(:ticket_multiselect_values) { options.values - trigger_values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when no value is selected' do
- let(:ticket_multiselect_values) { ['-'] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when all value is selected' do
- let(:ticket_multiselect_values) { options.values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value contains one of the trigger value' do
- let(:ticket_multiselect_values) { [trigger_values.first] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value does not contain one of the trigger value' do
- let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- end
- context "with 'contains one' used" do
- let(:operator) { 'contains one' }
- context 'when updated value is the same with trigger value' do
- let(:ticket_multiselect_values) { trigger_values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value is different from the trigger value' do
- let(:ticket_multiselect_values) { options.values - trigger_values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when no value is selected' do
- let(:ticket_multiselect_values) { ['-'] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when all value is selected' do
- let(:ticket_multiselect_values) { options.values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value contains only one of the trigger value' do
- let(:ticket_multiselect_values) { [trigger_values.first] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value does not contain one of the trigger value' do
- let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- end
- context "with 'contains all not' used" do
- let(:operator) { 'contains all not' }
- context 'when updated value is the same with trigger value' do
- let(:ticket_multiselect_values) { trigger_values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value is different from the trigger value' do
- let(:ticket_multiselect_values) { options.values - trigger_values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when no value is selected' do
- let(:ticket_multiselect_values) { ['-'] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when all value is selected' do
- let(:ticket_multiselect_values) { options.values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value contains only one of the trigger value' do
- let(:ticket_multiselect_values) { [trigger_values.first] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when updated value does not contain one of the trigger value' do
- let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- end
- context "with 'contains one not' used" do
- let(:operator) { 'contains one not' }
- context 'when updated value is the same with trigger value' do
- let(:ticket_multiselect_values) { trigger_values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value is different from the trigger value' do
- let(:ticket_multiselect_values) { options.values - trigger_values }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when no value is selected' do
- let(:ticket_multiselect_values) { ['-'] }
- it_behaves_like 'updating the ticket with the trigger condition'
- end
- context 'when all value is selected' do
- let(:ticket_multiselect_values) { options.values }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value contains only one of the trigger value' do
- let(:ticket_multiselect_values) { [trigger_values.first] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- context 'when updated value does not contain one of the trigger value' do
- let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
- it_behaves_like 'not updating the ticket with the trigger condition'
- end
- end
- end
- end
- end
|