by_overview_spec.rb 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Gql::Queries::Tickets::Cached::ByOverview, :aggregate_failures, type: :graphql do
  4. context 'when fetching cached ticket overviews' do
  5. let(:agent) { create(:agent) }
  6. let(:query) do
  7. <<~QUERY
  8. query ticketsCachedByOverview(
  9. $overviewId: ID!
  10. $orderBy: String
  11. $orderDirection: EnumOrderDirection
  12. $cursor: String
  13. $pageSize: Int
  14. $knownCollectionSignature: String
  15. $cacheTtl: Int!
  16. $renewCache: Boolean
  17. ) {
  18. ticketsCachedByOverview(
  19. overviewId: $overviewId
  20. orderBy: $orderBy
  21. orderDirection: $orderDirection
  22. after: $cursor
  23. first: $pageSize
  24. knownCollectionSignature: $knownCollectionSignature
  25. cacheTtl: $cacheTtl
  26. renewCache: $renewCache
  27. ) {
  28. totalCount
  29. collectionSignature
  30. edges {
  31. node {
  32. id
  33. internalId
  34. number
  35. articleCount
  36. stateColorCode
  37. escalationAt
  38. firstResponseEscalationAt
  39. updateEscalationAt
  40. closeEscalationAt
  41. firstResponseAt
  42. closeAt
  43. timeUnit
  44. lastCloseAt
  45. lastContactAt
  46. lastContactAgentAt
  47. lastContactCustomerAt
  48. policy {
  49. update
  50. agentReadAccess
  51. }
  52. }
  53. }
  54. }
  55. }
  56. QUERY
  57. end
  58. let(:known_collection_signature) { nil }
  59. let(:cache_ttl) { 4.seconds }
  60. # make sure that we can display many tickets with our query
  61. let(:page_size) { 1000 } # this would trigger an error without the custom complexity calculation
  62. let(:variables) { { pageSize: page_size, overviewId: gql.id(overview), knownCollectionSignature: known_collection_signature, cacheTtl: cache_ttl } }
  63. let(:overview) { Overview.find_by(link: 'all_unassigned') }
  64. let!(:ticket) { create(:ticket) }
  65. context 'with an agent', authenticated_as: :agent do
  66. def trace_queries(queries, &block)
  67. callback = lambda do |*, payload|
  68. queries[payload[:name]] ||= 0
  69. queries[payload[:name]] += 1
  70. end
  71. ActiveSupport::Notifications.subscribed(callback, 'sql.active_record', &block)
  72. queries
  73. end
  74. def ensure_no_ticket_queries(&block)
  75. queries = trace_queries({}, &block)
  76. expect(queries).not_to have_key('Ticket Load')
  77. expect(queries).not_to have_key('Ticket Count')
  78. end
  79. def ensure_ticket_queries(&block)
  80. queries = trace_queries({}, &block)
  81. expect(queries).to have_key('Ticket Load')
  82. expect(queries).to have_key('Ticket Count')
  83. end
  84. def ensure_fragment_writes
  85. allow(GraphQL::FragmentCache.cache_store).to receive(:write_multi).and_call_original
  86. yield
  87. expect(GraphQL::FragmentCache.cache_store).to have_received(:write_multi)
  88. end
  89. def ensure_no_fragment_writes
  90. allow(GraphQL::FragmentCache.cache_store).to receive(:write_multi).and_call_original
  91. yield
  92. expect(GraphQL::FragmentCache.cache_store).not_to have_received(:write_multi)
  93. end
  94. let(:agent) { create(:agent, groups: [ticket.group]) }
  95. it 'creates a cache on first call' do
  96. ensure_fragment_writes do
  97. ensure_ticket_queries do
  98. gql.execute(query, variables:)
  99. end
  100. end
  101. expect(gql.result.nodes.first).to include('number' => ticket.number)
  102. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  103. end
  104. it 'uses a cache on second call' do
  105. gql.execute(query, variables:)
  106. ensure_no_fragment_writes do
  107. ensure_no_ticket_queries do
  108. gql.execute(query, variables:)
  109. end
  110. end
  111. expect(gql.result.nodes.first).to include('number' => ticket.number)
  112. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  113. end
  114. it 'recreates the cache on second call if renewCache is provided' do
  115. gql.execute(query, variables:)
  116. ensure_fragment_writes do
  117. ensure_ticket_queries do
  118. gql.execute(query, variables: variables.merge({ renewCache: true }))
  119. end
  120. end
  121. expect(gql.result.nodes.first).to include('number' => ticket.number)
  122. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  123. end
  124. it 'recreates the cache on second call if cache has expired' do
  125. freeze_time
  126. gql.execute(query, variables:)
  127. travel(cache_ttl)
  128. ensure_fragment_writes do
  129. ensure_ticket_queries do
  130. gql.execute(query, variables:)
  131. end
  132. end
  133. expect(gql.result.nodes.first).to include('number' => ticket.number)
  134. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  135. end
  136. it 'creates another cache on second call if different cacheTtl is provided' do
  137. gql.execute(query, variables:)
  138. ensure_fragment_writes do
  139. ensure_ticket_queries do
  140. gql.execute(query, variables: variables.merge({ cacheTtl: cache_ttl + 1 }))
  141. end
  142. end
  143. expect(gql.result.nodes.first).to include('number' => ticket.number)
  144. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  145. end
  146. context 'with a different user with different permissions' do
  147. let(:other_agent) { create(:agent) }
  148. it 'does not use the cache on second call' do
  149. gql.execute(query, variables:)
  150. gql.graphql_current_user = other_agent
  151. ensure_fragment_writes do
  152. ensure_ticket_queries do
  153. gql.execute(query, variables:)
  154. end
  155. end
  156. expect(gql.result.nodes).to eq([])
  157. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  158. end
  159. end
  160. context 'with a different user with same permissions' do
  161. let(:other_agent) { create(:agent, groups: [ticket.group]) }
  162. context 'with non-personalized overview' do
  163. it 'uses the cache on second call' do
  164. gql.execute(query, variables:)
  165. gql.graphql_current_user = other_agent
  166. ensure_no_fragment_writes do
  167. ensure_no_ticket_queries do
  168. gql.execute(query, variables:)
  169. end
  170. end
  171. expect(gql.result.nodes.first).to include('number' => ticket.number)
  172. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  173. end
  174. end
  175. context 'with personalized overview' do
  176. let(:overview) { Overview.find_by(link: 'my_assigned') }
  177. it 'does not use the cache on second call' do
  178. gql.execute(query, variables:)
  179. gql.graphql_current_user = other_agent
  180. ensure_fragment_writes do
  181. ensure_ticket_queries do
  182. gql.execute(query, variables:)
  183. end
  184. end
  185. expect(gql.result.nodes).to eq([])
  186. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  187. end
  188. end
  189. end
  190. context 'when knownCollectionSignature is provided' do
  191. it 'does not use existing cache on second call, because knownCollectionSignature changes' do
  192. gql.execute(query, variables:)
  193. ensure_fragment_writes do
  194. ensure_ticket_queries do
  195. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  196. end
  197. end
  198. expect(gql.result.data[:edges]).to be_nil
  199. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  200. end
  201. it 'uses cache on third call' do
  202. gql.execute(query, variables:)
  203. known_collection_signature = gql.result.data[:collectionSignature]
  204. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  205. ensure_no_fragment_writes do
  206. ensure_no_ticket_queries do
  207. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  208. end
  209. end
  210. expect(gql.result.data[:edges]).to be_nil
  211. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  212. end
  213. it 'skips edges on second call without cache' do
  214. gql.execute(query, variables:)
  215. Rails.cache.clear
  216. ensure_fragment_writes do
  217. ensure_ticket_queries do
  218. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  219. end
  220. end
  221. expect(gql.result.data[:edges]).to be_nil
  222. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  223. end
  224. end
  225. end
  226. context 'with a customer', authenticated_as: :customer do
  227. let(:customer) { create(:customer) }
  228. it 'raises authorization error' do
  229. gql.execute(query, variables:)
  230. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  231. end
  232. end
  233. context 'without authenticated user' do
  234. before do
  235. gql.execute(query, variables:)
  236. end
  237. it_behaves_like 'graphql responds with error if unauthenticated'
  238. end
  239. end
  240. end