by_overview_spec.rb 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  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. context 'with an overview with complex conditions' do
  96. let(:overview) { create(:overview, :condition_expert) }
  97. it 'creates a cache on first call' do
  98. ensure_fragment_writes do
  99. ensure_ticket_queries do
  100. gql.execute(query, variables:)
  101. end
  102. end
  103. expect(gql.result.nodes.first).to include('number' => ticket.number)
  104. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  105. end
  106. end
  107. it 'creates a cache on first call' do
  108. ensure_fragment_writes do
  109. ensure_ticket_queries do
  110. gql.execute(query, variables:)
  111. end
  112. end
  113. expect(gql.result.nodes.first).to include('number' => ticket.number)
  114. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  115. end
  116. it 'uses a cache on second call' do
  117. gql.execute(query, variables:)
  118. ensure_no_fragment_writes do
  119. ensure_no_ticket_queries do
  120. gql.execute(query, variables:)
  121. end
  122. end
  123. expect(gql.result.nodes.first).to include('number' => ticket.number)
  124. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  125. end
  126. it 'recreates the cache on second call if renewCache is provided' do
  127. gql.execute(query, variables:)
  128. ensure_fragment_writes do
  129. ensure_ticket_queries do
  130. gql.execute(query, variables: variables.merge({ renewCache: true }))
  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 'recreates the cache on second call if cache has expired' do
  137. freeze_time
  138. gql.execute(query, variables:)
  139. travel(cache_ttl)
  140. ensure_fragment_writes do
  141. ensure_ticket_queries do
  142. gql.execute(query, variables:)
  143. end
  144. end
  145. expect(gql.result.nodes.first).to include('number' => ticket.number)
  146. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  147. end
  148. it 'creates another cache on second call if different cacheTtl is provided' do
  149. gql.execute(query, variables:)
  150. ensure_fragment_writes do
  151. ensure_ticket_queries do
  152. gql.execute(query, variables: variables.merge({ cacheTtl: cache_ttl + 1 }))
  153. end
  154. end
  155. expect(gql.result.nodes.first).to include('number' => ticket.number)
  156. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  157. end
  158. context 'with a different user with different permissions' do
  159. let(:other_agent) { create(:agent) }
  160. it 'does not use the cache on second call' do
  161. gql.execute(query, variables:)
  162. gql.graphql_current_user = other_agent
  163. ensure_fragment_writes do
  164. ensure_ticket_queries do
  165. gql.execute(query, variables:)
  166. end
  167. end
  168. expect(gql.result.nodes).to eq([])
  169. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  170. end
  171. end
  172. context 'with a different user with same permissions' do
  173. let(:other_agent) { create(:agent, groups: [ticket.group]) }
  174. context 'with non-personalized overview' do
  175. it 'uses the cache on second call' do
  176. gql.execute(query, variables:)
  177. gql.graphql_current_user = other_agent
  178. ensure_no_fragment_writes do
  179. ensure_no_ticket_queries do
  180. gql.execute(query, variables:)
  181. end
  182. end
  183. expect(gql.result.nodes.first).to include('number' => ticket.number)
  184. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  185. end
  186. end
  187. context 'with personalized overview' do
  188. let(:overview) { Overview.find_by(link: 'my_assigned') }
  189. it 'does not use the cache on second call' do
  190. gql.execute(query, variables:)
  191. gql.graphql_current_user = other_agent
  192. ensure_fragment_writes do
  193. ensure_ticket_queries do
  194. gql.execute(query, variables:)
  195. end
  196. end
  197. expect(gql.result.nodes).to eq([])
  198. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  199. end
  200. end
  201. end
  202. context 'when knownCollectionSignature is provided' do
  203. it 'does not use existing cache on second call, because knownCollectionSignature changes' do
  204. gql.execute(query, variables:)
  205. ensure_fragment_writes do
  206. ensure_ticket_queries do
  207. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  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 'uses cache on third call' do
  214. gql.execute(query, variables:)
  215. known_collection_signature = gql.result.data[:collectionSignature]
  216. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  217. ensure_no_fragment_writes do
  218. ensure_no_ticket_queries do
  219. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  220. end
  221. end
  222. expect(gql.result.data[:edges]).to be_nil
  223. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  224. end
  225. it 'skips edges on second call without cache' do
  226. gql.execute(query, variables:)
  227. Rails.cache.clear
  228. ensure_fragment_writes do
  229. ensure_ticket_queries do
  230. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  231. end
  232. end
  233. expect(gql.result.data[:edges]).to be_nil
  234. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  235. end
  236. end
  237. end
  238. context 'with a customer', authenticated_as: :customer do
  239. let(:customer) { create(:customer) }
  240. it 'raises authorization error' do
  241. gql.execute(query, variables:)
  242. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  243. end
  244. end
  245. context 'without authenticated user' do
  246. before do
  247. gql.execute(query, variables:)
  248. end
  249. it_behaves_like 'graphql responds with error if unauthenticated'
  250. end
  251. end
  252. end