123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974 |
- require 'rails_helper'
- require 'models/application_model_examples'
- require 'models/concerns/can_be_imported_examples'
- require 'models/concerns/can_csv_import_examples'
- require 'models/concerns/has_history_examples'
- require 'models/concerns/has_tags_examples'
- require 'models/concerns/has_xss_sanitized_note_examples'
- require 'models/concerns/has_object_manager_attributes_validation_examples'
- RSpec.describe Ticket, type: :model do
- it_behaves_like 'ApplicationModel'
- it_behaves_like 'CanBeImported'
- it_behaves_like 'CanCsvImport'
- it_behaves_like 'HasHistory', history_relation_object: 'Ticket::Article'
- it_behaves_like 'HasTags'
- it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
- it_behaves_like 'HasObjectManagerAttributesValidation'
- subject(:ticket) { create(:ticket) }
- describe 'Class methods:' do
- describe '.selectors' do
- # https://github.com/zammad/zammad/issues/1769
- context 'when matching multiple tickets, each with multiple articles' do
- let(:tickets) { create_list(:ticket, 2) }
- before do
- create(:ticket_article, ticket: tickets.first, from: 'asdf1@blubselector.de')
- create(:ticket_article, ticket: tickets.first, from: 'asdf2@blubselector.de')
- create(:ticket_article, ticket: tickets.first, from: 'asdf3@blubselector.de')
- create(:ticket_article, ticket: tickets.last, from: 'asdf4@blubselector.de')
- create(:ticket_article, ticket: tickets.last, from: 'asdf5@blubselector.de')
- create(:ticket_article, ticket: tickets.last, from: 'asdf6@blubselector.de')
- end
- let(:condition) do
- {
- 'article.from' => {
- operator: 'contains',
- value: 'blubselector.de',
- },
- }
- end
- it 'returns a list of unique tickets (i.e., no duplicates)' do
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to match_array([2, tickets.to_a])
- end
- end
- end
- end
- describe 'Instance methods:' do
- describe '#merge_to' do
- let(:target_ticket) { create(:ticket) }
- context 'when source ticket has Links' do
- let(:linked_tickets) { create_list(:ticket, 3) }
- let(:links) { linked_tickets.map { |l| create(:link, from: ticket, to: l) } }
- it 'reassigns all links to the target ticket after merge' do
- expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
- .to change { links.each(&:reload).map(&:link_object_source_value) }
- .to(Array.new(3) { target_ticket.id })
- end
- end
- context 'when attempting to cross-merge (i.e., to merge B → A after merging A → B)' do
- before { target_ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
- it 'raises an error' do
- expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
- .to raise_error('ticket already merged, no merge into merged ticket possible')
- end
- end
- context 'when attempting to self-merge (i.e., to merge A → A)' do
- it 'raises an error' do
- expect { ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
- .to raise_error("Can't merge ticket with it self!")
- end
- end
- # Issue #2469 - Add information "Ticket merged" to History
- context 'when merging' do
- let(:merge_user) { create(:user) }
- before do
- # create target ticket early
- # to avoid a race condition
- # when creating the history entries
- target_ticket
- travel 5.minutes
- end
- it 'creates history entries in both the origin ticket and the target ticket' do
- ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
- expect(target_ticket.history_get.size).to eq 2
- target_history = target_ticket.history_get.last
- expect(target_history['object']).to eq 'Ticket'
- expect(target_history['type']).to eq 'received_merge'
- expect(target_history['created_by_id']).to eq merge_user.id
- expect(target_history['o_id']).to eq target_ticket.id
- expect(target_history['id_to']).to eq target_ticket.id
- expect(target_history['id_from']).to eq ticket.id
- expect(ticket.history_get.size).to eq 4
- origin_history = ticket.reload.history_get[1]
- expect(origin_history['object']).to eq 'Ticket'
- expect(origin_history['type']).to eq 'merged_into'
- expect(origin_history['created_by_id']).to eq merge_user.id
- expect(origin_history['o_id']).to eq ticket.id
- expect(origin_history['id_to']).to eq target_ticket.id
- expect(origin_history['id_from']).to eq ticket.id
- end
- it 'sends ExternalSync.migrate' do
- allow(ExternalSync).to receive(:migrate)
- ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
- expect(ExternalSync).to have_received(:migrate).with('Ticket', ticket.id, target_ticket.id)
- end
- end
- end
- describe '#perform_changes' do
- # Regression test for https://github.com/zammad/zammad/issues/2001
- describe 'argument handling' do
- let(:perform) do
- {
- 'notification.email' => {
- body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
- recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
- subject: "Autoclose (\#{ticket.title})"
- }
- }
- end
- it 'does not mutate contents of "perform" hash' do
- expect { ticket.perform_changes(perform, 'trigger', {}, 1) }
- .not_to change { perform }
- end
- end
- context 'with "ticket.state_id" key in "perform" hash' do
- let(:perform) do
- {
- 'ticket.state_id' => {
- 'value' => Ticket::State.lookup(name: 'closed').id
- }
- }
- end
- it 'changes #state to specified value' do
- expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
- .to change { ticket.reload.state.name }.to('closed')
- end
- end
- context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
- let(:perform) do
- {
- 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
- 'ticket.action' => { 'value' => 'delete' },
- }
- end
- it 'performs a ticket deletion on a ticket' do
- expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
- .to change(ticket, :destroyed?).to(true)
- end
- end
- context 'with a "notification.email" trigger' do
- # Regression test for https://github.com/zammad/zammad/issues/1543
- #
- # If a new article fires an email notification trigger,
- # and then another article is added to the same ticket
- # before that trigger is performed,
- # the email template's 'article' var should refer to the originating article,
- # not the newest one.
- #
- # (This occurs whenever one action fires multiple email notification triggers.)
- context 'when two articles are created before the trigger fires once (race condition)' do
- let!(:article) { create(:ticket_article, ticket: ticket) }
- let!(:new_article) { create(:ticket_article, ticket: ticket) }
- let(:trigger) do
- build(:trigger,
- perform: {
- 'notification.email' => {
- body: '',
- recipient: 'ticket_customer',
- subject: ''
- }
- })
- end
- # required by Ticket#perform_changes for email notifications
- before { article.ticket.group.update(email_address: create(:email_address)) }
- it 'passes the first article to NotificationFactory::Mailer' do
- expect(NotificationFactory::Mailer)
- .to receive(:template)
- .with(hash_including(objects: { ticket: ticket, article: article }))
- .at_least(:once)
- .and_call_original
- expect(NotificationFactory::Mailer)
- .not_to receive(:template)
- .with(hash_including(objects: { ticket: ticket, article: new_article }))
- ticket.perform_changes(trigger.perform, 'trigger', { article_id: article.id }, 1)
- end
- end
- end
- context 'with a notification trigger' do
- # https://github.com/zammad/zammad/issues/2782
- #
- # Notification triggers should log notification as private or public
- # according to given configuration
- let(:user) { create(:admin_user, mobile: '+37061010000') }
- before { ticket.group.users << user }
- let(:perform) do
- {
- notification_key => {
- body: 'Old programmers never die. They just branch to a new address.',
- recipient: 'ticket_agents',
- subject: 'Old programmers never die. They just branch to a new address.'
- }
- }.deep_merge(additional_options).deep_stringify_keys
- end
- let(:notification_key) { "notification.#{notification_type}" }
- shared_examples 'verify log visibility status' do
- shared_examples 'notification trigger' do
- it 'adds Ticket::Article' do
- expect { ticket.perform_changes(perform, 'trigger', ticket, user) }
- .to change { ticket.articles.count }.by(1)
- end
- it 'new Ticket::Article visibility reflects setting' do
- ticket.perform_changes(perform, 'trigger', ticket, User.first)
- new_article = ticket.articles.reload.last
- expect(new_article.internal).to be target_internal_value
- end
- end
- context 'when set to private' do
- let(:additional_options) do
- {
- notification_key => {
- internal: true
- }
- }
- end
- let(:target_internal_value) { true }
- it_behaves_like 'notification trigger'
- end
- context 'when set to internal' do
- let(:additional_options) do
- {
- notification_key => {
- internal: false
- }
- }
- end
- let(:target_internal_value) { false }
- it_behaves_like 'notification trigger'
- end
- context 'when no selection was made' do # ensure previously created triggers default to public
- let(:additional_options) do
- {}
- end
- let(:target_internal_value) { false }
- it_behaves_like 'notification trigger'
- end
- end
- context 'dispatching email' do
- let(:notification_type) { :email }
- include_examples 'verify log visibility status'
- end
- context 'dispatching SMS' do
- let(:notification_type) { :sms }
- before { create(:channel, area: 'Sms::Notification') }
- include_examples 'verify log visibility status'
- end
- end
- end
- describe '#subject_build' do
- context 'with default "ticket_hook_position" setting ("right")' do
- it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
- expect(ticket.subject_build('foo'))
- .to eq("foo [Ticket##{ticket.number}]")
- end
- context 'and a non-default value for the "ticket_hook" setting' do
- before { Setting.set('ticket_hook', 'bar baz') }
- it 'replaces "Ticket#" with the new ticket hook' do
- expect(ticket.subject_build('foo'))
- .to eq("foo [bar baz#{ticket.number}]")
- end
- end
- context 'and a non-default value for the "ticket_hook_divider" setting' do
- before { Setting.set('ticket_hook_divider', ': ') }
- it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
- expect(ticket.subject_build('foo'))
- .to eq("foo [Ticket#: #{ticket.number}]")
- end
- end
- context 'when the given string already contains a ticket reference, but in the wrong place' do
- it 'moves the ticket reference to the end' do
- expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
- .to eq("foo [Ticket##{ticket.number}]")
- end
- end
- context 'when the given string already contains an alternately formatted ticket reference' do
- it 'reformats the ticket reference' do
- expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
- .to eq("foo [Ticket##{ticket.number}]")
- end
- end
- end
- context 'with alternate "ticket_hook_position" setting ("left")' do
- before { Setting.set('ticket_hook_position', 'left') }
- it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
- expect(ticket.subject_build('foo'))
- .to eq("[Ticket##{ticket.number}] foo")
- end
- context 'and a non-default value for the "ticket_hook" setting' do
- before { Setting.set('ticket_hook', 'bar baz') }
- it 'replaces "Ticket#" with the new ticket hook' do
- expect(ticket.subject_build('foo'))
- .to eq("[bar baz#{ticket.number}] foo")
- end
- end
- context 'and a non-default value for the "ticket_hook_divider" setting' do
- before { Setting.set('ticket_hook_divider', ': ') }
- it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
- expect(ticket.subject_build('foo'))
- .to eq("[Ticket#: #{ticket.number}] foo")
- end
- end
- context 'when the given string already contains a ticket reference, but in the wrong place' do
- it 'moves the ticket reference to the start' do
- expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
- .to eq("[Ticket##{ticket.number}] foo")
- end
- end
- context 'when the given string already contains an alternately formatted ticket reference' do
- it 'reformats the ticket reference' do
- expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
- .to eq("[Ticket##{ticket.number}] foo")
- end
- end
- end
- end
- end
- describe 'Attributes:' do
- describe '#owner' do
- let(:original_owner) { create(:agent_user, groups: [ticket.group]) }
- before { ticket.update(owner: original_owner) }
- context 'when assigned directly' do
- context 'to an active agent belonging to ticket.group' do
- let(:agent) { create(:agent_user, groups: [ticket.group]) }
- it 'can be set' do
- expect { ticket.update(owner: agent) }
- .to change { ticket.reload.owner }.to(agent)
- end
- end
- context 'to an agent not belonging to ticket.group' do
- let(:agent) { create(:agent_user, groups: [other_group]) }
- let(:other_group) { create(:group) }
- it 'resets to default user (id: 1) instead' do
- expect { ticket.update(owner: agent) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- context 'to an inactive agent' do
- let(:agent) { create(:agent_user, groups: [ticket.group], active: false) }
- it 'resets to default user (id: 1) instead' do
- expect { ticket.update(owner: agent) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- context 'to a non-agent' do
- let(:agent) { create(:customer_user, groups: [ticket.group]) }
- it 'resets to default user (id: 1) instead' do
- expect { ticket.update(owner: agent) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- end
- context 'when the ticket is updated for any other reason' do
- context 'if original owner is still an active agent belonging to ticket.group' do
- it 'does not change' do
- expect { create(:ticket_article, ticket: ticket) }
- .not_to change { ticket.reload.owner }
- end
- end
- context 'if original owner has left ticket.group' do
- before { original_owner.groups = [] }
- it 'resets to default user (id: 1)' do
- expect { create(:ticket_article, ticket: ticket) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- context 'if original owner has become inactive' do
- before { original_owner.update(active: false) }
- it 'resets to default user (id: 1)' do
- expect { create(:ticket_article, ticket: ticket) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- context 'if original owner has lost agent status' do
- before { original_owner.roles = [create(:role)] }
- it 'resets to default user (id: 1)' do
- expect { create(:ticket_article, ticket: ticket) }
- .to change { ticket.reload.owner }.to(User.first)
- end
- end
- context 'when the Ticket is closed' do
- before do
- ticket.update!(state: Ticket::State.lookup(name: 'closed'))
- end
- context 'if original owner is still an active agent belonging to ticket.group' do
- it 'does not change' do
- expect { create(:ticket_article, ticket: ticket) }
- .not_to change { ticket.reload.owner }
- end
- end
- context 'if original owner has left ticket.group' do
- before { original_owner.groups = [] }
- it 'does not change' do
- expect { create(:ticket_article, ticket: ticket) }
- .not_to change { ticket.reload.owner }
- end
- end
- context 'if original owner has become inactive' do
- before { original_owner.update(active: false) }
- it 'does not change' do
- expect { create(:ticket_article, ticket: ticket) }
- .not_to change { ticket.reload.owner }
- end
- end
- context 'if original owner has lost agent status' do
- before { original_owner.roles = [create(:role)] }
- it 'does not change' do
- expect { create(:ticket_article, ticket: ticket) }
- .not_to change { ticket.reload.owner }
- end
- end
- end
- end
- end
- describe '#state' do
- context 'when originally "new" (default)' do
- context 'and a customer article is added' do
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
- it 'stays "new"' do
- expect { article }
- .not_to change { ticket.state.name }.from('new')
- end
- end
- context 'and a non-customer article is added' do
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'switches to "open"' do
- expect { article }
- .to change { ticket.reload.state.name }.from('new').to('open')
- end
- end
- end
- context 'when originally "closed"' do
- before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
- context 'when a non-customer article is added' do
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'stays "closed"' do
- expect { article }.not_to change { ticket.reload.state.name }
- end
- end
- end
- end
- describe '#pending_time' do
- subject(:ticket) { create(:ticket, pending_time: Time.zone.now + 2.days) }
- context 'when #state is updated to any non-"pending" value' do
- it 'is reset to nil' do
- expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
- .to change(ticket, :pending_time).to(nil)
- end
- end
- # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
- context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
- it 'does not prematurely raise within the callback (#reset_pending_time)' do
- expect { ticket.update!(state: nil) }
- .to raise_error(ActiveRecord::StatementInvalid)
- end
- end
- end
- describe '#escalation_at' do
- before { travel_to(Time.current) } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
- let(:calendar) { create(:calendar, :'24/7') }
- context 'with no SLAs in the system' do
- it 'defaults to nil' do
- expect(ticket.escalation_at).to be(nil)
- end
- end
- context 'with an SLA in the system' do
- before { sla } # create sla
- it 'is set based on SLA’s #first_response_time' do
- expect(ticket.reload.escalation_at.to_i)
- .to eq(1.hour.from_now.to_i)
- end
- context 'after first agent’s response' do
- before { ticket } # create ticket
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'is updated based on the SLA’s #update_time' do
- travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
- expect { article }
- .to change { ticket.reload.escalation_at.to_i }
- .to eq(3.hours.from_now.to_i)
- end
- context 'when new #update_time is later than original #solution_time' do
- it 'is updated based on the original #solution_time' do
- travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
- expect { article }
- .to change { ticket.reload.escalation_at.to_i }
- .to eq(4.hours.after(ticket.created_at).to_i)
- end
- end
- end
- end
- context 'when updated after an SLA has been added to the system' do
- before do
- ticket # create ticket
- sla # create sla
- end
- it 'is updated based on the new SLA’s #first_response_time' do
- expect { ticket.save! }
- .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
- end
- end
- context 'when updated after all SLAs have been removed from the system' do
- before do
- sla # create sla
- ticket # create ticket
- sla.destroy
- end
- it 'is set to nil' do
- expect { ticket.save! }
- .to change { ticket.reload.escalation_at }.to(nil)
- end
- end
- end
- describe '#first_response_escalation_at' do
- before { travel_to(Time.current) } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
- let(:calendar) { create(:calendar, :'24/7') }
- context 'with no SLAs in the system' do
- it 'defaults to nil' do
- expect(ticket.first_response_escalation_at).to be(nil)
- end
- end
- context 'with an SLA in the system' do
- before { sla } # create sla
- it 'is set based on SLA’s #first_response_time' do
- expect(ticket.reload.first_response_escalation_at.to_i)
- .to eq(1.hour.from_now.to_i)
- end
- context 'after first agent’s response' do
- before { ticket } # create ticket
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'does not change' do
- expect { article }.not_to change(ticket, :first_response_escalation_at)
- end
- end
- end
- end
- describe '#update_escalation_at' do
- before { travel_to(Time.current) } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
- let(:calendar) { create(:calendar, :'24/7') }
- context 'with no SLAs in the system' do
- it 'defaults to nil' do
- expect(ticket.update_escalation_at).to be(nil)
- end
- end
- context 'with an SLA in the system' do
- before { sla } # create sla
- it 'is set based on SLA’s #update_time' do
- expect(ticket.reload.update_escalation_at.to_i)
- .to eq(3.hours.from_now.to_i)
- end
- context 'after first agent’s response' do
- before { ticket } # create ticket
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'is updated based on the SLA’s #update_time' do
- travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
- expect { article }
- .to change { ticket.reload.update_escalation_at.to_i }
- .to(3.hours.from_now.to_i)
- end
- end
- end
- end
- describe '#close_escalation_at' do
- before { travel_to(Time.current) } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
- let(:calendar) { create(:calendar, :'24/7') }
- context 'with no SLAs in the system' do
- it 'defaults to nil' do
- expect(ticket.close_escalation_at).to be(nil)
- end
- end
- context 'with an SLA in the system' do
- before { sla } # create sla
- it 'is set based on SLA’s #solution_time' do
- expect(ticket.reload.close_escalation_at.to_i)
- .to eq(4.hours.from_now.to_i)
- end
- context 'after first agent’s response' do
- before { ticket } # create ticket
- let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
- it 'does not change' do
- expect { article }.not_to change(ticket, :close_escalation_at)
- end
- end
- end
- end
- end
- describe 'Associations:' do
- describe '#organization' do
- subject(:ticket) { build(:ticket, customer: customer, organization: nil) }
- let(:customer) { create(:customer, :with_org) }
- context 'on creation' do
- it 'automatically adopts the organization of its #customer' do
- expect { ticket.save }
- .to change(ticket, :organization).to(customer.organization)
- end
- end
- context 'on update of #customer.organization' do
- context 'to nil' do
- it 'automatically updates to #customer’s new value' do
- ticket.save
- expect { customer.update(organization: nil) }
- .to change { ticket.reload.organization }.to(nil)
- end
- end
- context 'to a different organization' do
- let(:new_org) { create(:organization) }
- it 'automatically updates to #customer’s new value' do
- ticket.save
- expect { customer.update(organization: new_org) }
- .to change { ticket.reload.organization }.to(new_org)
- end
- end
- end
- end
- end
- describe 'Callbacks & Observers -' do
- describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
- it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
- expect { create(:ticket, title: "some title \u0000 123") }
- .not_to raise_error
- end
- end
- describe 'XSS protection:' do
- subject(:ticket) { create(:ticket, title: title) }
- let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
- it 'does not sanitize title' do
- expect(ticket.title).to eq(title)
- end
- end
- describe 'Cti::CallerId syncing:' do
- subject(:ticket) { build(:ticket) }
- before { allow(Cti::CallerId).to receive(:build) }
- it 'adds numbers in article bodies (via Cti::CallerId.build)' do
- expect(Cti::CallerId).to receive(:build).with(ticket)
- ticket.save
- Observer::Transaction.commit
- Scheduler.worker(true)
- end
- end
- describe 'Touching associations on update:' do
- subject(:ticket) { create(:ticket, customer: customer) }
- let(:customer) { create(:customer_user, organization: organization) }
- let(:organization) { create(:organization) }
- let(:other_customer) { create(:customer_user, organization: other_organization) }
- let(:other_organization) { create(:organization) }
- context 'on creation' do
- it 'touches its customer and his organization' do
- expect { ticket }
- .to change { customer.reload.updated_at }
- .and change { organization.reload.updated_at }
- end
- end
- context 'on destruction' do
- before { ticket }
- it 'touches its customer and his organization' do
- expect { ticket.destroy }
- .to change { customer.reload.updated_at }
- .and change { organization.reload.updated_at }
- end
- end
- context 'when customer association is changed' do
- it 'touches both old and new customer, and their organizations' do
- expect { ticket.update(customer: other_customer) }
- .to change { customer.reload.updated_at }
- .and change { organization.reload.updated_at }
- .and change { other_customer.reload.updated_at }
- .and change { other_organization.reload.updated_at }
- end
- end
- end
- describe 'Association & attachment management:' do
- it 'deletes all related ActivityStreams on destroy' do
- create_list(:activity_stream, 3, o: ticket)
- expect { ticket.destroy }
- .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
- .to(false)
- end
- it 'deletes all related Links on destroy' do
- create(:link, from: ticket, to: create(:ticket))
- create(:link, from: create(:ticket), to: ticket)
- create(:link, from: ticket, to: create(:ticket))
- expect { ticket.destroy }
- .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
- .to(false)
- end
- it 'deletes all related Articles on destroy' do
- create_list(:ticket_article, 3, ticket: ticket)
- expect { ticket.destroy }
- .to change { Ticket::Article.exists?(ticket: ticket) }
- .to(false)
- end
- it 'deletes all related OnlineNotifications on destroy' do
- create_list(:online_notification, 3, o: ticket)
- expect { ticket.destroy }
- .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
- .to(false)
- end
- it 'deletes all related Tags on destroy' do
- create_list(:tag, 3, o: ticket)
- expect { ticket.destroy }
- .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
- .to(false)
- end
- it 'deletes all related Histories on destroy' do
- create_list(:history, 3, o: ticket)
- expect { ticket.destroy }
- .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
- .to(false)
- end
- it 'deletes all related Karma::ActivityLogs on destroy' do
- create_list(:'karma/activity_log', 3, o: ticket)
- expect { ticket.destroy }
- .to change { Karma::ActivityLog.exists?(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
- .to(false)
- end
- it 'deletes all related RecentViews on destroy' do
- create_list(:recent_view, 3, o: ticket)
- expect { ticket.destroy }
- .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
- .to(false)
- end
- context 'when ticket is generated from email (with attachments)' do
- subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
- let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail001.box')) }
- it 'adds attachments to the Store{::File,::Provider::DB} tables' do
- expect { ticket }
- .to change(Store, :count).by(2)
- .and change { Store::File.count }.by(2)
- .and change { Store::Provider::DB.count }.by(2)
- end
- context 'and subsequently destroyed' do
- it 'deletes all related attachments' do
- ticket # create ticket
- expect { ticket.destroy }
- .to change(Store, :count).by(-2)
- .and change { Store::File.count }.by(-2)
- .and change { Store::Provider::DB.count }.by(-2)
- end
- end
- context 'and a duplicate ticket is generated from the same email' do
- before { ticket } # create ticket
- let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
- it 'adds duplicate attachments to the Store table only' do
- expect { duplicate }
- .to change(Store, :count).by(2)
- .and change { Store::File.count }.by(0)
- .and change { Store::Provider::DB.count }.by(0)
- end
- context 'when only the duplicate ticket is destroyed' do
- it 'deletes only the duplicate attachments' do
- duplicate # create ticket
- expect { duplicate.destroy }
- .to change(Store, :count).by(-2)
- .and change { Store::File.count }.by(0)
- .and change { Store::Provider::DB.count }.by(0)
- end
- it 'deletes all related attachments' do
- duplicate.destroy
- expect { ticket.destroy }
- .to change(Store, :count).by(-2)
- .and change { Store::File.count }.by(-2)
- .and change { Store::Provider::DB.count }.by(-2)
- end
- end
- end
- end
- end
- end
- end
|