123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
- require 'rails_helper'
- RSpec.describe Gql::Queries::Tickets::Cached::ByOverview, :aggregate_failures, type: :graphql do
- context 'when fetching cached ticket overviews' do
- let(:agent) { create(:agent) }
- let(:query) do
- <<~QUERY
- query ticketsCachedByOverview(
- $overviewId: ID!
- $orderBy: String
- $orderDirection: EnumOrderDirection
- $cursor: String
- $pageSize: Int
- $knownCollectionSignature: String
- $cacheTtl: Int!
- $renewCache: Boolean
- ) {
- ticketsCachedByOverview(
- overviewId: $overviewId
- orderBy: $orderBy
- orderDirection: $orderDirection
- after: $cursor
- first: $pageSize
- knownCollectionSignature: $knownCollectionSignature
- cacheTtl: $cacheTtl
- renewCache: $renewCache
- ) {
- totalCount
- collectionSignature
- edges {
- node {
- id
- internalId
- number
- articleCount
- stateColorCode
- escalationAt
- firstResponseEscalationAt
- updateEscalationAt
- closeEscalationAt
- firstResponseAt
- closeAt
- timeUnit
- lastCloseAt
- lastContactAt
- lastContactAgentAt
- lastContactCustomerAt
- policy {
- update
- agentReadAccess
- }
- }
- }
- }
- }
- QUERY
- end
- let(:known_collection_signature) { nil }
- let(:cache_ttl) { 4.seconds }
- # make sure that we can display many tickets with our query
- let(:page_size) { 1000 } # this would trigger an error without the custom complexity calculation
- let(:variables) { { pageSize: page_size, overviewId: gql.id(overview), knownCollectionSignature: known_collection_signature, cacheTtl: cache_ttl } }
- let(:overview) { Overview.find_by(link: 'all_unassigned') }
- let!(:ticket) { create(:ticket) }
- context 'with an agent', authenticated_as: :agent do
- def trace_queries(queries, &block)
- callback = lambda do |*, payload|
- queries[payload[:name]] ||= 0
- queries[payload[:name]] += 1
- end
- ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)
- queries
- end
- def ensure_no_ticket_queries(&block)
- queries = trace_queries({}, &block)
- expect(queries).not_to have_key('Ticket Load')
- expect(queries).not_to have_key('Ticket Count')
- end
- def ensure_ticket_queries(&block)
- queries = trace_queries({}, &block)
- expect(queries).to have_key('Ticket Load')
- expect(queries).to have_key('Ticket Count')
- end
- def ensure_fragment_writes
- allow(GraphQL::FragmentCache.cache_store).to receive(:write_multi).and_call_original
- yield
- expect(GraphQL::FragmentCache.cache_store).to have_received(:write_multi)
- end
- def ensure_no_fragment_writes
- allow(GraphQL::FragmentCache.cache_store).to receive(:write_multi).and_call_original
- yield
- expect(GraphQL::FragmentCache.cache_store).not_to have_received(:write_multi)
- end
- let(:agent) { create(:agent, groups: [ticket.group]) }
- it 'creates a cache on first call' do
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'uses a cache on second call' do
- gql.execute(query, variables:)
- ensure_no_fragment_writes do
- ensure_no_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'recreates the cache on second call if renewCache is provided' do
- gql.execute(query, variables:)
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables: variables.merge({ renewCache: true }))
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'recreates the cache on second call if cache has expired' do
- freeze_time
- gql.execute(query, variables:)
- travel(cache_ttl)
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'creates another cache on second call if different cacheTtl is provided' do
- gql.execute(query, variables:)
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables: variables.merge({ cacheTtl: cache_ttl + 1 }))
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- context 'with a different user with different permissions' do
- let(:other_agent) { create(:agent) }
- it 'does not use the cache on second call' do
- gql.execute(query, variables:)
- gql.graphql_current_user = other_agent
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes).to eq([])
- expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
- end
- end
- context 'with a different user with same permissions' do
- let(:other_agent) { create(:agent, groups: [ticket.group]) }
- context 'with non-personalized overview' do
- it 'uses the cache on second call' do
- gql.execute(query, variables:)
- gql.graphql_current_user = other_agent
- ensure_no_fragment_writes do
- ensure_no_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes.first).to include('number' => ticket.number)
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- end
- context 'with personalized overview' do
- let(:overview) { Overview.find_by(link: 'my_assigned') }
- it 'does not use the cache on second call' do
- gql.execute(query, variables:)
- gql.graphql_current_user = other_agent
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables:)
- end
- end
- expect(gql.result.nodes).to eq([])
- expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
- end
- end
- end
- context 'when knownCollectionSignature is provided' do
- it 'does not use existing cache on second call, because knownCollectionSignature changes' do
- gql.execute(query, variables:)
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
- end
- end
- expect(gql.result.data[:edges]).to be_nil
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'uses cache on third call' do
- gql.execute(query, variables:)
- known_collection_signature = gql.result.data[:collectionSignature]
- gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
- ensure_no_fragment_writes do
- ensure_no_ticket_queries do
- gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
- end
- end
- expect(gql.result.data[:edges]).to be_nil
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- it 'skips edges on second call without cache' do
- gql.execute(query, variables:)
- Rails.cache.clear
- ensure_fragment_writes do
- ensure_ticket_queries do
- gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
- end
- end
- expect(gql.result.data[:edges]).to be_nil
- expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
- end
- end
- end
- context 'with a customer', authenticated_as: :customer do
- let(:customer) { create(:customer) }
- it 'raises authorization error' do
- gql.execute(query, variables:)
- expect(gql.result.error_type).to eq(Exceptions::Forbidden)
- end
- end
- context 'without authenticated user' do
- before do
- gql.execute(query, variables:)
- end
- it_behaves_like 'graphql responds with error if unauthenticated'
- end
- end
- end
|