1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- 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/can_csv_import_ticket_examples'
- require 'models/concerns/checks_core_workflow_examples'
- require 'models/concerns/has_history_examples'
- require 'models/concerns/has_tags_examples'
- require 'models/concerns/has_taskbars_examples'
- require 'models/concerns/has_xss_sanitized_note_examples'
- require 'models/concerns/has_object_manager_attributes_examples'
- require 'models/tag/writes_to_ticket_history_examples'
- require 'models/ticket/enqueues_user_ticket_counter_job_examples'
- require 'models/ticket/escalation_examples'
- require 'models/ticket/resets_pending_time_seconds_examples'
- require 'models/ticket/sets_close_time_examples'
- require 'models/ticket/sets_last_owner_update_time_examples'
- RSpec.describe Ticket, type: :model do
- subject(:ticket) { create(:ticket) }
- it_behaves_like 'ApplicationModel', can_param: { sample_data_attribute: :title }
- it_behaves_like 'CanBeImported'
- it_behaves_like 'CanCsvImport'
- include_examples 'CanCsvImport - Ticket specific tests'
- it_behaves_like 'ChecksCoreWorkflow'
- it_behaves_like 'HasHistory', history_relation_object: ['Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom', 'Checklist', 'Checklist::Item']
- it_behaves_like 'HasTags'
- it_behaves_like 'TagWritesToTicketHistory'
- it_behaves_like 'HasTaskbars'
- it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
- it_behaves_like 'HasObjectManagerAttributes'
- it_behaves_like 'Ticket::Escalation'
- it_behaves_like 'TicketEnqueuesTicketUserTicketCounterJob'
- it_behaves_like 'TicketResetsPendingTimeSeconds'
- it_behaves_like 'TicketSetsCloseTime'
- it_behaves_like 'TicketSetsLastOwnerUpdateTime'
- it_behaves_like 'Association clears cache', association: :articles, factory: :ticket_article
- 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) }
- let(:condition) do
- {
- 'article.from' => {
- operator: 'contains',
- value: 'blubselector.de',
- },
- }
- end
- 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
- it 'returns a list of unique tickets (i.e., no duplicates)' do
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to contain_exactly(2, tickets.to_a)
- end
- end
- context 'when customer has multiple organizations' do
- let(:organization1) { create(:organization) }
- let(:organization2) { create(:organization) }
- let(:organization3) { create(:organization) }
- let(:customer) { create(:customer, organization: organization1, organizations: [organization2, organization3]) }
- let(:ticket1) { create(:ticket, customer: customer, organization: organization1) }
- let(:ticket2) { create(:ticket, customer: customer, organization: organization2) }
- let(:ticket3) { create(:ticket, customer: customer, organization: organization3) }
- before do
- ticket1 && ticket2 && ticket3
- end
- context 'when current user organization is used' do
- let(:condition) do
- {
- 'ticket.organization_id' => {
- operator: 'is', # is not
- pre_condition: 'current_user.organization_id',
- },
- }
- end
- it 'returns the customer tickets' do
- expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: customer))
- .to contain_exactly(3, include(ticket1, ticket2, ticket3))
- end
- 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('It is not possible to merge into an already merged ticket.')
- 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('A ticket cannot be merged into itself.')
- end
- end
- context 'when both tickets are linked with the same parent (parent->child)' do
- let(:parent) { create(:ticket) }
- before do
- create(:link,
- link_type: 'child',
- link_object_source_value: ticket.id,
- link_object_target_value: parent.id)
- create(:link,
- link_type: 'child',
- link_object_source_value: target_ticket.id,
- link_object_target_value: parent.id)
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- it 'does remove the link from the merged ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: ticket.id
- )
- expect(links.count).to eq(1) # one link to the source ticket (no parent link)
- end
- it 'does not remove the link from the target ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: target_ticket.id
- )
- expect(links.count).to eq(2) # one link to the merged ticket + parent link
- end
- end
- context 'when both tickets are linked with the same parent (child->parent)' do
- let(:parent) { create(:ticket) }
- before do
- create(:link,
- link_type: 'child',
- link_object_source_value: parent.id,
- link_object_target_value: ticket.id)
- create(:link,
- link_type: 'child',
- link_object_source_value: parent.id,
- link_object_target_value: target_ticket.id)
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- it 'does remove the link from the merged ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: ticket.id
- )
- expect(links.count).to eq(1) # one link to the source ticket (no parent link)
- end
- it 'does not remove the link from the target ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: target_ticket.id
- )
- expect(links.count).to eq(2) # one link to the merged ticket + parent link
- end
- end
- context 'when both tickets are linked with the same parent (different link types)' do
- let(:parent) { create(:ticket) }
- before do
- create(:link,
- link_type: 'normal',
- link_object_source_value: parent.id,
- link_object_target_value: ticket.id)
- create(:link,
- link_type: 'child',
- link_object_source_value: parent.id,
- link_object_target_value: target_ticket.id)
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- it 'does remove the link from the merged ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: ticket.id
- )
- expect(links.count).to eq(1) # one link to the source ticket (no normal link)
- end
- it 'does not remove the link from the target ticket' do
- links = Link.list(
- link_object: 'Ticket',
- link_object_value: target_ticket.id
- )
- expect(links.count).to eq(3) # one lin to the merged ticket + parent link + normal link
- end
- end
- context 'when both tickets having mentions to the same user' do
- let(:watcher) { create(:agent, groups: [ticket.group, target_ticket.group]) }
- before do
- create(:mention, mentionable: ticket, user: watcher)
- create(:mention, mentionable: target_ticket, user: watcher)
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- it 'does remove the link from the merged ticket' do
- expect(target_ticket.mentions.count).to eq(1) # one mention to watcher user
- end
- end
- context 'when merging a ticket with mentioned user who has no access to the target ticket' do
- let(:watcher) { create(:agent, groups: [ticket.group]) }
- it 'does remove the link from the merged ticket' do
- create(:mention, mentionable: ticket, user: watcher)
- expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
- .to change { target_ticket.mentions.count }
- .to(1)
- end
- end
- 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
- ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
- end
- # Issue #2469 - Add information "Ticket merged" to History
- it 'creates history entries in both the origin ticket and the target ticket' do
- 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
- # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
- context 'and deleting the origin ticket' do
- it 'adds reference number and title to the target ticket' do
- expect { ticket.destroy }
- .to change { target_ticket.history_get.find { |elem| elem.fetch('type') == 'received_merge' }['value_from'] }
- .to("##{ticket.number} #{ticket.title}")
- end
- end
- # Issue #2960 - Ticket removal of merged / linked tickets doesn't remove references
- context 'and deleting the target ticket' do
- it 'adds reference number and title to the origin ticket' do
- expect { target_ticket.destroy }
- .to change { ticket.history_get.find { |elem| elem.fetch('type') == 'merged_into' }['value_to'] }
- .to("##{target_ticket.number} #{target_ticket.title}")
- end
- end
- end
- # https://github.com/zammad/zammad/issues/3105
- context 'when merge actions triggers exist', :performs_jobs do
- before do
- ticket && target_ticket
- merged_into_trigger && received_merge_trigger && update_trigger
- allow_any_instance_of(described_class).to receive(:perform_changes) do |ticket, trigger|
- log << { ticket: ticket.id, trigger: trigger.id }
- end
- perform_enqueued_jobs do
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- end
- let(:merged_into_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update.merged_into') }
- let(:received_merge_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update.received_merge') }
- let(:update_trigger) { create(:trigger, :conditionable, condition_ticket_action: 'update') }
- let(:log) { [] }
- it 'merge_into triggered with source ticket' do
- expect(log).to include({ ticket: ticket.id, trigger: merged_into_trigger.id })
- end
- it 'received_merge not triggered with source ticket' do
- expect(log).not_to include({ ticket: ticket.id, trigger: received_merge_trigger.id })
- end
- it 'update not triggered with source ticket' do
- expect(log).not_to include({ ticket: ticket.id, trigger: update_trigger.id })
- end
- it 'merge_into not triggered with target ticket' do
- expect(log).not_to include({ ticket: target_ticket.id, trigger: merged_into_trigger.id })
- end
- it 'received_merge triggered with target ticket' do
- expect(log).to include({ ticket: target_ticket.id, trigger: received_merge_trigger.id })
- end
- it 'update not triggered with target ticket' do
- expect(log).not_to include({ ticket: target_ticket.id, trigger: update_trigger.id })
- end
- end
- # https://github.com/zammad/zammad/issues/3105
- context 'when user has notifications enabled', :performs_jobs do
- before do
- user
- allow(OnlineNotification).to receive(:add) do |**args|
- next if args[:object] != 'Ticket'
- log << { type: :online, event: args[:type], ticket_id: args[:o_id], user_id: args[:user_id] }
- end
- allow(NotificationFactory::Mailer).to receive(:notification) do |**args|
- log << { type: :email, event: args[:template], ticket_id: args[:objects][:ticket].id, user_id: args[:user].id }
- end
- perform_enqueued_jobs do
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- end
- let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
- let(:log) { [] }
- it 'merge_into notification sent with source ticket' do
- expect(log).to include({ type: :online, event: 'update.merged_into', ticket_id: ticket.id, user_id: user.id })
- end
- it 'received_merge notification not sent with source ticket' do
- expect(log).not_to include({ type: :online, event: 'update.received_merge', ticket_id: ticket.id, user_id: user.id })
- end
- it 'update notification not sent with source ticket' do
- expect(log).not_to include({ type: :online, event: 'update', ticket_id: ticket.id, user_id: user.id })
- end
- it 'merge_into notification not sent with target ticket' do
- expect(log).not_to include({ type: :online, event: 'update.merged_into', ticket_id: target_ticket.id, user_id: user.id })
- end
- it 'received_merge notification sent with target ticket' do
- expect(log).to include({ type: :online, event: 'update.received_merge', ticket_id: target_ticket.id, user_id: user.id })
- end
- it 'update notification not sent with target ticket' do
- expect(log).not_to include({ type: :online, event: 'update', ticket_id: target_ticket.id, user_id: user.id })
- end
- it 'merge_into email sent with source ticket' do
- expect(log).to include({ type: :email, event: 'ticket_update_merged_into', ticket_id: ticket.id, user_id: user.id })
- end
- it 'received_merge email not sent with source ticket' do
- expect(log).not_to include({ type: :email, event: 'ticket_update_received_merge', ticket_id: ticket.id, user_id: user.id })
- end
- it 'update email not sent with source ticket' do
- expect(log).not_to include({ type: :email, event: 'ticket_update', ticket_id: ticket.id, user_id: user.id })
- end
- it 'merge_into email not sent with target ticket' do
- expect(log).not_to include({ type: :email, event: 'ticket_update_merged_into', ticket_id: target_ticket.id, user_id: user.id })
- end
- it 'received_merge email sent with target ticket' do
- expect(log).to include({ type: :email, event: 'ticket_update_received_merge', ticket_id: target_ticket.id, user_id: user.id })
- end
- it 'update email not sent with target ticket' do
- expect(log).not_to include({ type: :email, event: 'ticket_update', ticket_id: target_ticket.id, user_id: user.id })
- end
- end
- # https://github.com/zammad/zammad/issues/3105
- context 'when sending notification email correct template', :performs_jobs do
- before do
- user
- allow(NotificationFactory::Mailer).to receive(:deliver) do |**args|
- log << args[:subject]
- end
- perform_enqueued_jobs do
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- end
- end
- let(:user) { create(:agent, :preferencable, notification_group_ids: [ticket, target_ticket].map(&:group_id), groups: [ticket, target_ticket].map(&:group)) }
- let(:log) { [] }
- it 'is used for merged_into' do
- expect(log).to include(start_with("Ticket (#{ticket.title}) was merged into another ticket"))
- end
- it 'is used for received_merge' do
- expect(log).to include(start_with("Another ticket was merged into ticket (#{target_ticket.title})"))
- end
- end
- context 'ApplicationHandleInfo context' do
- it 'gets switched to "merge"' do
- allow(ApplicationHandleInfo).to receive('context=')
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- expect(ApplicationHandleInfo).to have_received('context=').with('merge').at_least(1)
- end
- it 'reverts back to default' do
- allow(ApplicationHandleInfo).to receive('context=')
- ticket.merge_to(ticket_id: target_ticket.id, user_id: 1)
- expect(ApplicationHandleInfo.context).not_to eq 'merge'
- 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
- describe '#last_original_update_at' do
- let(:result) { ticket.last_original_update_at }
- it 'returns initial customer enquiry time when customer contacted repeatedly' do
- ticket
- target = create(:ticket_article, :inbound_email, ticket: ticket)
- travel 10.minutes
- create(:ticket_article, :inbound_email, ticket: ticket)
- expect(result).to eq target.created_at
- end
- it 'returns agent contact time when customer did not respond to agent reach out' do
- ticket
- create(:ticket_article, :outbound_email, ticket: ticket)
- expect(result).to eq ticket.last_contact_agent_at
- end
- it 'returns nil if no customer response' do
- ticket
- expect(result).to be_nil
- end
- context 'with customer enquiry and agent response' do
- before do
- ticket
- create(:ticket_article, :inbound_email, ticket: ticket)
- travel 10.minutes
- create(:ticket_article, :outbound_email, ticket: ticket)
- travel 10.minutes
- end
- it 'returns last customer enquiry time when agent did not respond yet' do
- target = create(:ticket_article, :inbound_email, ticket: ticket)
- expect(result).to eq target.created_at
- end
- it 'returns agent response time when agent responded to customer enquiry' do
- expect(result).to eq ticket.last_contact_agent_at
- end
- end
- end
- describe '#param_cleanup' do
- it 'does only remove parameters which are invalid and not the complete params hash if one element is invalid (#3743)' do
- expect(described_class.param_cleanup({ state_id: 3, customer_id: 'guess:1234' }, true, false, false)).to eq({ 'state_id' => 3 })
- end
- end
- end
- describe 'Attributes:' do
- describe '#owner' do
- let(:original_owner) { create(:agent, 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, 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, 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, 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, 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_list(:role, 1) }
- it 'resets to default user (id: 1)' do
- Rails.cache.clear
- 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_list(:role, 1) }
- 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: 2.days.from_now) }
- 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 { freeze_time } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_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 #close_escalation_at' 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(ticket.reload.close_escalation_at)
- 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(4.hours.after(ticket.created_at))
- 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
- context 'when within last (relative)' do
- let(:first_response_time) { 5 }
- let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
- let(:within_condition) do
- { 'ticket.escalation_at'=>{ 'operator' => 'within last (relative)', 'value' => '30', 'range' => 'minute' } }
- end
- before do
- sla
- travel_to '2020-11-05 11:37:00'
- ticket = create(:ticket)
- create(:ticket_article, :inbound_email, ticket: ticket)
- travel_to '2020-11-05 11:50:00'
- end
- context 'when in range' do
- it 'does find the ticket' do
- count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
- expect(count).to eq(1)
- end
- end
- context 'when out of range' do
- let(:first_response_time) { 500 }
- it 'does not find the ticket' do
- count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
- expect(count).to eq(0)
- end
- end
- end
- context 'when till (relative)' do
- let(:first_response_time) { 5 }
- let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
- let(:condition) do
- { 'ticket.escalation_at'=>{ 'operator' => 'till (relative)', 'value' => '30', 'range' => 'minute' } }
- end
- before do
- sla
- travel_to '2020-11-05 11:37:00'
- ticket = create(:ticket)
- create(:ticket_article, :inbound_email, ticket: ticket)
- travel_to '2020-11-05 11:50:00'
- end
- context 'when in range' do
- it 'does find the ticket' do
- count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
- expect(count).to eq(1)
- end
- end
- context 'when out of range' do
- let(:first_response_time) { 500 }
- it 'does not find the ticket' do
- count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
- expect(count).to eq(0)
- end
- end
- end
- context 'when from (relative)' do
- let(:first_response_time) { 5 }
- let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
- let(:condition) do
- { 'ticket.escalation_at'=>{ 'operator' => 'from (relative)', 'value' => '30', 'range' => 'minute' } }
- end
- before do
- sla
- travel_to '2020-11-05 11:37:00'
- ticket = create(:ticket)
- create(:ticket_article, :inbound_email, ticket: ticket)
- end
- context 'when in range' do
- it 'does find the ticket' do
- travel_to '2020-11-05 11:50:00'
- count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
- expect(count).to eq(1)
- end
- end
- context 'when out of range' do
- let(:first_response_time) { 5 }
- it 'does not find the ticket' do
- travel_to '2020-11-05 13:50:00'
- count, _tickets = described_class.selectors(condition, limit: 2_000, execution_time: true)
- expect(count).to eq(0)
- end
- end
- end
- context 'when within next (relative)' do
- let(:first_response_time) { 5 }
- let(:sla) { create(:sla, calendar: calendar, first_response_time: first_response_time) }
- let(:within_condition) do
- { 'ticket.escalation_at'=>{ 'operator' => 'within next (relative)', 'value' => '30', 'range' => 'minute' } }
- end
- before do
- sla
- travel_to '2020-11-05 11:50:00'
- ticket = create(:ticket)
- create(:ticket_article, :inbound_email, ticket: ticket)
- travel_to '2020-11-05 11:37:00'
- end
- context 'when in range' do
- it 'does find the ticket' do
- count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
- expect(count).to eq(1)
- end
- end
- context 'when out of range' do
- let(:first_response_time) { 500 }
- it 'does not find the ticket' do
- count, _tickets = described_class.selectors(within_condition, limit: 2_000, execution_time: true)
- expect(count).to eq(0)
- end
- end
- end
- end
- describe '#first_response_escalation_at' do
- before { freeze_time } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_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 'is cleared' do
- expect { article }.to change { ticket.reload.first_response_escalation_at }.to(nil)
- end
- end
- end
- end
- describe '#update_escalation_at' do
- before { freeze_time } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_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
- travel 1.minute
- create(:ticket_article, ticket: ticket, sender_name: 'Customer')
- 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
- create(:ticket_article, ticket: ticket, sender_name: 'Customer')
- travel(1.minute)
- expect { article }
- .to change { ticket.reload.update_escalation_at }
- .to(nil)
- end
- end
- end
- end
- describe '#close_escalation_at' do
- before { freeze_time } # freeze time
- let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_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 '.search' do
- shared_examples 'search permissions' do
- let(:group) { create(:group) }
- before do
- ticket
- end
- shared_examples 'permitted' do
- it 'finds Ticket' do
- expect(described_class.search(query: ticket.number, current_user: current_user).count).to eq(1)
- end
- end
- shared_examples 'no permission' do
- it "doesn't find Ticket" do
- expect(described_class.search(query: ticket.number, current_user: current_user)).to be_blank
- end
- end
- context 'Agent with Group access' do
- let(:ticket) do
- ticket = create(:ticket, group: group)
- create(:ticket_article, ticket: ticket)
- ticket
- end
- let(:current_user) { create(:agent, groups: [group]) }
- it_behaves_like 'permitted'
- end
- context 'when Agent is Customer of Ticket' do
- let(:ticket) do
- ticket = create(:ticket, customer: current_user)
- create(:ticket_article, ticket: ticket)
- ticket
- end
- let(:current_user) { create(:agent_and_customer) }
- it_behaves_like 'permitted'
- end
- context 'for Organization access' do
- let(:ticket) do
- ticket = create(:ticket, customer: customer)
- create(:ticket_article, ticket: ticket)
- ticket
- end
- let(:customer) { create(:customer, organization: organization) }
- context 'when Organization is shared' do
- let(:organization) { create(:organization, shared: true) }
- context 'for unrelated Agent' do
- let(:current_user) { create(:agent) }
- it_behaves_like 'no permission'
- end
- context 'for Agent in same Organization' do
- let(:current_user) { create(:agent_and_customer, organization: organization) }
- it_behaves_like 'permitted'
- end
- context 'for Customer of Ticket' do
- let(:current_user) { customer }
- it_behaves_like 'permitted'
- end
- end
- context 'when Organization is not shared' do
- let(:organization) { create(:organization, shared: false) }
- context 'for unrelated Agent' do
- let(:current_user) { create(:agent) }
- it_behaves_like 'no permission'
- end
- context 'for Agent in same Organization' do
- let(:current_user) { create(:agent_and_customer, organization: organization) }
- it_behaves_like 'no permission'
- end
- context 'for Customer of Ticket' do
- let(:current_user) { customer }
- it_behaves_like 'permitted'
- end
- end
- end
- end
- context 'with searchindex', searchindex: true do
- include_examples 'search permissions' do
- before do
- searchindex_model_reload([described_class])
- end
- end
- end
- context 'without searchindex' do
- before do
- Setting.set('es_url', nil)
- end
- include_examples 'search permissions'
- 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:', performs_jobs: true 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
- perform_enqueued_jobs commit_transaction: true
- end
- end
- describe 'Touching associations on update:' do
- subject(:ticket) { create(:ticket, customer: customer) }
- let(:customer) { create(:customer, organization: organization) }
- let(:organization) { create(:organization) }
- let(:other_customer) { create(:customer, 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 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
- it 'destroys all related dependencies', current_user_id: 1 do
- refs_known = {
- 'Ticket::Article' => { 'ticket_id' => 1 },
- 'Ticket::TimeAccounting' => { 'ticket_id' => 1 },
- 'Ticket::SharedDraftZoom' => { 'ticket_id' => 0 },
- 'Checklist::Item' => { 'ticket_id' => 1 },
- }
- ticket = create(:ticket)
- article = create(:ticket_article, ticket: ticket)
- accounting = create(:ticket_time_accounting, ticket: ticket)
- checklist = create(:checklist, ticket: ticket)
- checklist_item = create(:checklist_item, ticket_id: ticket.id)
- refs_ticket = Models.references('Ticket', ticket.id, true)
- expect(refs_ticket).to eq(refs_known)
- ticket.destroy
- expect { ticket.reload }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { article.reload }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { accounting.reload }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { checklist.reload }.to raise_exception(ActiveRecord::RecordNotFound)
- # Related checklist_item should not be destroyed
- expect(checklist_item.reload.ticket_id).to be_nil
- end
- context 'when ticket is generated from email (with attachments)' do
- subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
- let(:raw_email) { Rails.root.join('test/data/mail/mail001.box').read }
- 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 not_change(Store::File, :count)
- .and not_change(Store::Provider::DB, :count)
- 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 not_change(Store::File, :count)
- .and not_change(Store::Provider::DB, :count)
- 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
- describe 'Ticket lifecycle order-of-operations:', performs_jobs: true do
- subject!(:ticket) { create(:ticket) }
- let!(:agent) { create(:agent, groups: [group]) }
- let(:group) { create(:group) }
- before do
- create(
- :trigger,
- condition: { 'ticket.action' => { 'operator' => 'is', 'value' => 'create' } },
- perform: { 'ticket.group_id' => { 'value' => group.id } }
- )
- end
- it 'fires triggers before new ticket notifications are sent' do
- expect { TransactionDispatcher.commit }
- .to change { ticket.reload.group }.to(group)
- expect { perform_enqueued_jobs }
- .to change { NotificationFactory::Mailer.already_sent?(ticket, agent, 'email') }.to(1)
- end
- end
- describe 'Ticket has changed attributes:' do
- subject!(:ticket) { create(:ticket) }
- let(:group) { create(:group) }
- let(:condition_field) { nil }
- shared_examples 'updated ticket group with trigger condition' do
- it 'updated ticket group with has changed trigger condition' do
- expect { TransactionDispatcher.commit }.to change { ticket.reload.group }.to(group)
- end
- end
- before do
- create(
- :trigger,
- condition: { "ticket.#{condition_field}" => { 'operator' => 'has changed', 'value' => 'create' } },
- perform: { 'ticket.group_id' => { 'value' => group.id } }
- )
- ticket.update!(condition_field => Time.zone.now)
- end
- context "when changing 'first_response_at' attribute" do
- let(:condition_field) { 'first_response_at' }
- include_examples 'updated ticket group with trigger condition'
- end
- context "when changing 'close_at' attribute" do
- let(:condition_field) { 'close_at' }
- include_examples 'updated ticket group with trigger condition'
- end
- context "when changing 'last_contact_agent_at' attribute" do
- let(:condition_field) { 'last_contact_agent_at' }
- include_examples 'updated ticket group with trigger condition'
- end
- context "when changing 'last_contact_customer_at' attribute" do
- let(:condition_field) { 'last_contact_customer_at' }
- include_examples 'updated ticket group with trigger condition'
- end
- context "when changing 'last_contact_at' attribute" do
- let(:condition_field) { 'last_contact_at' }
- include_examples 'updated ticket group with trigger condition'
- end
- end
- end
- describe 'Mentions:', sends_notification_emails: true do
- context 'when notifications', performs_jobs: true do
- let(:prefs_matrix_no_mentions) do
- { 'notification_config' =>
- { 'matrix' =>
- { 'create' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
- 'update' => { 'criteria' => { 'owned_by_me' => true, 'owned_by_nobody' => true, 'subscribed' => false, 'no' => true }, 'channel' => { 'email' => true, 'online' => true } },
- 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
- 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => false, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
- end
- let(:prefs_matrix_only_mentions) do
- { 'notification_config' =>
- { 'matrix' =>
- { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
- 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
- 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
- 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } } } }
- end
- let(:prefs_matrix_only_mentions_groups) do
- { 'notification_config' =>
- { 'matrix' =>
- { 'create' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
- 'update' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => true, 'online' => true } },
- 'reminder_reached' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } },
- 'escalation' => { 'criteria' => { 'owned_by_me' => false, 'owned_by_nobody' => false, 'subscribed' => true, 'no' => false }, 'channel' => { 'email' => false, 'online' => false } } },
- 'group_ids' => [create(:group).id, create(:group).id, create(:group).id] } }
- end
- let(:mention_group) { create(:group) }
- let(:no_access_group) { create(:group) }
- let(:user_only_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions) }
- let(:user_read_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_only_mentions_groups) }
- let(:user_no_mentions) { create(:agent, groups: [mention_group], preferences: prefs_matrix_no_mentions) }
- let(:ticket) { create(:ticket, group: mention_group, owner: user_no_mentions) }
- it 'does inform mention user about the ticket update' do
- create(:mention, mentionable: ticket, user: user_only_mentions)
- create(:mention, mentionable: ticket, user: user_read_mentions)
- create(:mention, mentionable: ticket, user: user_no_mentions)
- perform_enqueued_jobs commit_transaction: true
- check_notification do
- ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
- perform_enqueued_jobs commit_transaction: true
- sent(
- template: 'ticket_update',
- user: user_no_mentions,
- )
- sent(
- template: 'ticket_update',
- user: user_read_mentions,
- )
- sent(
- template: 'ticket_update',
- user: user_only_mentions,
- )
- end
- end
- it 'does not inform mention user about the ticket update' do
- ticket
- perform_enqueued_jobs commit_transaction: true
- check_notification do
- ticket.update(priority: Ticket::Priority.find_by(name: '3 high'))
- perform_enqueued_jobs commit_transaction: true
- sent(
- template: 'ticket_update',
- user: user_no_mentions,
- )
- not_sent(
- template: 'ticket_update',
- user: user_read_mentions,
- )
- not_sent(
- template: 'ticket_update',
- user: user_only_mentions,
- )
- end
- end
- it 'does inform mention user about ticket creation' do
- check_notification do
- ticket = create(:ticket, owner: user_no_mentions, group: mention_group)
- create(:mention, mentionable: ticket, user: user_read_mentions)
- create(:mention, mentionable: ticket, user: user_only_mentions)
- perform_enqueued_jobs commit_transaction: true
- sent(
- template: 'ticket_create',
- user: user_no_mentions,
- )
- sent(
- template: 'ticket_create',
- user: user_read_mentions,
- )
- sent(
- template: 'ticket_create',
- user: user_only_mentions,
- )
- end
- end
- it 'does not inform mention user about ticket creation' do
- check_notification do
- create(:ticket, owner: user_no_mentions, group: mention_group)
- perform_enqueued_jobs commit_transaction: true
- sent(
- template: 'ticket_create',
- user: user_no_mentions,
- )
- not_sent(
- template: 'ticket_create',
- user: user_read_mentions,
- )
- not_sent(
- template: 'ticket_create',
- user: user_only_mentions,
- )
- end
- end
- it 'does not inform mention user about ticket creation because of no permissions' do
- check_notification do
- ticket = create(:ticket, group: no_access_group)
- build(:mention, mentionable: ticket, user: user_read_mentions).save!(validate: false)
- build(:mention, mentionable: ticket, user: user_only_mentions).save!(validate: false)
- perform_enqueued_jobs commit_transaction: true
- not_sent(
- template: 'ticket_create',
- user: user_read_mentions,
- )
- not_sent(
- template: 'ticket_create',
- user: user_only_mentions,
- )
- end
- end
- end
- context 'selectors' do
- let(:mention_group) { create(:group) }
- let(:ticket_mentions) { create(:ticket, group: mention_group) }
- let(:ticket_normal) { create(:ticket, group: mention_group) }
- let(:user_mentions) { create(:agent, groups: [mention_group]) }
- let(:user_mentions_2) { create(:agent, groups: [mention_group]) }
- let(:user_no_mentions) { create(:agent, groups: [mention_group]) }
- before do
- described_class.destroy_all
- ticket_normal
- user_no_mentions
- create(:mention, mentionable: ticket_mentions, user: user_mentions)
- end
- it 'pre condition is not_set' do
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'not_set',
- operator: 'is',
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to contain_exactly(1, [ticket_normal])
- end
- it 'pre condition is not not_set' do
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'not_set',
- operator: 'is not',
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to contain_exactly(1, [ticket_mentions])
- end
- it 'pre condition is current_user.id' do
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'current_user.id',
- operator: 'is',
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
- .to contain_exactly(1, [ticket_mentions])
- end
- it 'pre condition is not current_user.id (one mention on one ticket)' do
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'current_user.id',
- operator: 'is not',
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
- .to contain_exactly(1, [ticket_normal])
- end
- it 'pre condition is not current_user.id (multiple mentions on one ticket)' do
- create(:mention, mentionable: ticket_mentions, user: user_mentions_2)
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'current_user.id',
- operator: 'is not',
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full', current_user: user_mentions))
- .to contain_exactly(1, [ticket_normal])
- end
- it 'pre condition is specific' do
- create(:mention, mentionable: ticket_mentions, user: user_mentions_2)
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'specific',
- operator: 'is',
- value: [user_mentions.id, user_mentions_2.id]
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to contain_exactly(1, [ticket_mentions].to_a)
- end
- it 'pre condition is not specific' do
- condition = {
- 'ticket.mention_user_ids' => {
- pre_condition: 'specific',
- operator: 'is not',
- value: [user_mentions.id, user_mentions_2.id]
- },
- }
- expect(described_class.selectors(condition, limit: 100, access: 'full'))
- .to contain_exactly(1, [ticket_normal])
- end
- end
- end
- describe '.search_index_attribute_lookup_oversized?' do
- subject!(:ticket) { create(:ticket) }
- context 'when payload is ok' do
- let(:current_payload_size) { 3.megabytes }
- it 'return false' do
- expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be false
- end
- end
- context 'when payload is bigger' do
- let(:current_payload_size) { 350.megabytes }
- it 'return true' do
- expect(ticket.send(:search_index_attribute_lookup_oversized?, current_payload_size)).to be true
- end
- end
- end
- describe '.search_index_attribute_lookup_file_oversized?' do
- subject!(:store) do
- create(:store,
- object: 'SomeObject',
- o_id: 1,
- data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
- filename: 'test.TXT')
- end
- context 'when total payload is ok' do
- let(:current_payload_size) { 200.megabytes }
- it 'return false' do
- expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be false
- end
- end
- context 'when total payload is oversized' do
- let(:current_payload_size) { 299.megabytes }
- it 'return true' do
- expect(ticket.send(:search_index_attribute_lookup_file_oversized?, store, current_payload_size)).to be true
- end
- end
- end
- describe '.search_index_attribute_lookup_file_ignored?' do
- context 'when attachment is indexable' do
- subject!(:store_with_indexable_extention) do
- create(:store,
- object: 'SomeObject',
- o_id: 1,
- data: 'some content',
- filename: 'test.TXT')
- end
- it 'return false' do
- expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_with_indexable_extention)).to be false
- end
- end
- context 'when attachment is no indexable' do
- subject!(:store_without_indexable_extention) do
- create(:store,
- object: 'SomeObject',
- o_id: 1,
- data: 'some content',
- filename: 'test.BIN')
- end
- it 'return true' do
- expect(ticket.send(:search_index_attribute_lookup_file_ignored?, store_without_indexable_extention)).to be true
- end
- end
- end
- describe '.search_index_article_attachment_attributes' do
- context 'payload for article' do
- subject!(:store_item) do
- create(:store,
- object: 'SomeObject',
- o_id: 1,
- data: 'some content',
- filename: 'test.TXT')
- end
- it 'verify count of attributes' do
- expect(ticket.send(:search_index_article_attachment_attributes, store_item).count).to be 3
- end
- it 'verify size' do
- expect(ticket.send(:search_index_article_attachment_attributes, store_item)['size']).to eq '12'
- end
- it 'verify _name' do
- expect(ticket.send(:search_index_article_attachment_attributes, store_item)['_name']).to eq 'test.TXT'
- end
- it 'verify _content' do
- expect(ticket.send(:search_index_article_attachment_attributes, store_item)['_content']).to eq 'c29tZSBjb250ZW50'
- end
- end
- end
- describe '.search_index_article_attributes' do
- context 'payload for attachment' do
- subject!(:ticket_article) do
- create(:ticket_article, ticket: create(:ticket))
- end
- it 'verify count of attributes' do
- expect(ticket.send(:search_index_article_attributes, ticket_article).count).to eq 20
- end
- it 'verify from' do
- expect(ticket.send(:search_index_article_attributes, ticket_article)['from']).to eq ticket_article.from
- end
- it 'verify body' do
- expect(ticket.send(:search_index_article_attributes, ticket_article)['body']).to eq ticket_article.body
- end
- end
- end
- describe '.search_index_attribute_lookup' do
- subject!(:ticket) { create(:ticket) }
- let(:search_index_attribute_lookup) do
- article1 = create(:ticket_article, ticket: ticket)
- create(:store,
- object: 'Ticket::Article',
- o_id: article1.id,
- data: 'some content',
- filename: 'some_file.bin',
- preferences: {
- 'Content-Type' => 'text/plain',
- })
- create(:store,
- object: 'Ticket::Article',
- o_id: article1.id,
- data: 'a' * ((1024**2) * 2.4), # with 2.4 mb
- filename: 'some_file.pdf',
- preferences: {
- 'Content-Type' => 'image/pdf',
- })
- create(:store,
- object: 'Ticket::Article',
- o_id: article1.id,
- data: 'a' * ((1024**2) * 5.8), # with 5,8 mb
- filename: 'some_file.txt',
- preferences: {
- 'Content-Type' => 'text/plain',
- })
- create(:ticket_article, ticket: ticket, body: 'a' * ((1024**2) * 1.2)) # body with 1,2 mb
- create(:ticket_article, ticket: ticket)
- ticket.search_index_attribute_lookup
- end
- context 'when es_attachment_max_size_in_mb takes all attachments' do
- before { Setting.set('es_attachment_max_size_in_mb', 15) }
- it 'verify count of articles' do
- expect(search_index_attribute_lookup['article'].count).to eq 3
- end
- it 'verify count of attachments' do
- expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 2
- end
- it 'verify if pdf exists' do
- expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
- end
- it 'verify if txt exists' do
- expect(search_index_attribute_lookup['article'][0]['attachment'][1]['_name']).to eq 'some_file.txt'
- end
- end
- context 'when es_attachment_max_size_in_mb takes only one attachment' do
- before { Setting.set('es_attachment_max_size_in_mb', 4) }
- it 'verify count of articles' do
- expect(search_index_attribute_lookup['article'].count).to eq 3
- end
- it 'verify count of attachments' do
- expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 1
- end
- it 'verify if pdf exists' do
- expect(search_index_attribute_lookup['article'][0]['attachment'][0]['_name']).to eq 'some_file.pdf'
- end
- end
- context 'when es_attachment_max_size_in_mb takes no attachment' do
- before { Setting.set('es_attachment_max_size_in_mb', 2) }
- it 'verify count of articles' do
- expect(search_index_attribute_lookup['article'].count).to eq 3
- end
- it 'verify count of attachments' do
- expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
- end
- end
- context 'when es_total_max_size_in_mb takes no attachment and no oversized article' do
- before { Setting.set('es_total_max_size_in_mb', 1) }
- it 'verify count of articles' do
- expect(search_index_attribute_lookup['article'].count).to eq 2
- end
- it 'verify count of attachments' do
- expect(search_index_attribute_lookup['article'][0]['attachment'].count).to eq 0
- end
- end
- end
- describe '#reopen_after_certain_time?' do
- context 'when groups.follow_up_possible is set to "new_ticket_after_certain_time"' do
- let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: 2) }
- context 'when ticket is open' do
- let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'open')) }
- it 'returns false' do
- expect(ticket.reopen_after_certain_time?).to be false
- end
- end
- context 'when ticket is closed' do
- let(:ticket) { create(:ticket, group: group, state: Ticket::State.find_by(name: 'closed')) }
- context 'when it is within configured time frame' do
- it 'returns true' do
- expect(ticket.reopen_after_certain_time?).to be true
- end
- end
- context 'when it is outside configured time frame' do
- before do
- ticket
- travel 3.days
- end
- it 'returns false' do
- expect(ticket.reopen_after_certain_time?).to be false
- end
- end
- end
- context 'when reopen_time_in_days is not set' do
- let(:group) { create(:group, follow_up_possible: 'new_ticket_after_certain_time', reopen_time_in_days: -1) }
- it 'returns false' do
- expect(ticket.reopen_after_certain_time?).to be false
- end
- end
- end
- end
- describe '#get_references' do
- let!(:ticket) { create(:ticket) }
- let!(:articles) { create_list(:ticket_article, 10, ticket: ticket, reply_to: nil) }
- before do
- articles.each do |article|
- article.update(message_id: SecureRandom.uuid)
- end
- end
- it 'does return references' do
- expect(ticket.get_references.count).to eq(10)
- end
- it 'does return references by limit' do
- expect(ticket.get_references([], max_length: articles.last.message_id.length * 3).count).to eq(3)
- end
- it 'does return last 3 references by limit' do
- expect(ticket.get_references([], max_length: articles.last.message_id.length * 3)).to eq(articles.map(&:message_id)[-3..])
- end
- it 'does ignore references' do
- expect(ticket.get_references([articles.last.message_id])).not_to include(articles.last.message_id)
- end
- end
- end
|