by_overview_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 'does not include renewCache in the cache key' do
  137. gql.execute(query, variables: variables.merge({ renewCache: true }))
  138. ensure_no_fragment_writes do
  139. ensure_no_ticket_queries do
  140. gql.execute(query, variables:)
  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. it 'recreates the cache on second call if cache has expired' do
  147. freeze_time
  148. gql.execute(query, variables:)
  149. travel(cache_ttl)
  150. ensure_fragment_writes do
  151. ensure_ticket_queries do
  152. gql.execute(query, variables:)
  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. it 'creates another cache on second call if different cacheTtl is provided' do
  159. gql.execute(query, variables:)
  160. ensure_fragment_writes do
  161. ensure_ticket_queries do
  162. gql.execute(query, variables: variables.merge({ cacheTtl: cache_ttl + 1 }))
  163. end
  164. end
  165. expect(gql.result.nodes.first).to include('number' => ticket.number)
  166. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  167. end
  168. context 'with a different user with different permissions' do
  169. let(:other_agent) { create(:agent) }
  170. it 'does not use the cache on second call' do
  171. gql.execute(query, variables:)
  172. gql.graphql_current_user = other_agent
  173. ensure_fragment_writes do
  174. ensure_ticket_queries do
  175. gql.execute(query, variables:)
  176. end
  177. end
  178. expect(gql.result.nodes).to eq([])
  179. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  180. end
  181. end
  182. context 'with a different user with same permissions' do
  183. let(:other_agent) { create(:agent, groups: [ticket.group]) }
  184. context 'with non-personalized overview' do
  185. it 'uses the cache on second call' do
  186. gql.execute(query, variables:)
  187. gql.graphql_current_user = other_agent
  188. ensure_no_fragment_writes do
  189. ensure_no_ticket_queries do
  190. gql.execute(query, variables:)
  191. end
  192. end
  193. expect(gql.result.nodes.first).to include('number' => ticket.number)
  194. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  195. end
  196. end
  197. context 'with personalized overview' do
  198. let(:overview) { Overview.find_by(link: 'my_assigned') }
  199. it 'does not use the cache on second call' do
  200. gql.execute(query, variables:)
  201. gql.graphql_current_user = other_agent
  202. ensure_fragment_writes do
  203. ensure_ticket_queries do
  204. gql.execute(query, variables:)
  205. end
  206. end
  207. expect(gql.result.nodes).to eq([])
  208. expect(gql.result.data).to include('totalCount' => 0, 'collectionSignature' => '[]')
  209. end
  210. end
  211. end
  212. context 'when knownCollectionSignature is provided' do
  213. it 'does not use existing cache on second call, because knownCollectionSignature changes' do
  214. gql.execute(query, variables:)
  215. ensure_fragment_writes do
  216. ensure_ticket_queries do
  217. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  218. end
  219. end
  220. expect(gql.result.data[:edges]).to be_nil
  221. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  222. end
  223. it 'uses cache on third call' do
  224. gql.execute(query, variables:)
  225. known_collection_signature = gql.result.data[:collectionSignature]
  226. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  227. ensure_no_fragment_writes do
  228. ensure_no_ticket_queries do
  229. gql.execute(query, variables: variables.merge({ knownCollectionSignature: known_collection_signature }))
  230. end
  231. end
  232. expect(gql.result.data[:edges]).to be_nil
  233. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  234. end
  235. it 'skips edges on second call without cache' do
  236. gql.execute(query, variables:)
  237. Rails.cache.clear
  238. ensure_fragment_writes do
  239. ensure_ticket_queries do
  240. gql.execute(query, variables: variables.merge({ knownCollectionSignature: gql.result.data[:collectionSignature] }))
  241. end
  242. end
  243. expect(gql.result.data[:edges]).to be_nil
  244. expect(gql.result.data).to include('totalCount' => 1, 'collectionSignature' => be_present)
  245. end
  246. end
  247. end
  248. context 'with a customer', authenticated_as: :customer do
  249. let(:customer) { create(:customer) }
  250. it 'raises authorization error' do
  251. gql.execute(query, variables:)
  252. expect(gql.result.error_type).to eq(Exceptions::Forbidden)
  253. end
  254. end
  255. context 'without authenticated user' do
  256. before do
  257. gql.execute(query, variables:)
  258. end
  259. it_behaves_like 'graphql responds with error if unauthenticated'
  260. end
  261. end
  262. end