summaryrefslogtreecommitdiff
path: root/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb')
-rw-r--r--spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb208
1 files changed, 208 insertions, 0 deletions
diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
new file mode 100644
index 00000000000..b6fe94a2312
--- /dev/null
+++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::CachingArrayResolver do
+ include GraphqlHelpers
+
+ let_it_be(:non_admins) { create_list(:user, 4, admin: false) }
+ let(:query_context) { {} }
+ let(:max_page_size) { 10 }
+ let(:field) { double('Field', max_page_size: max_page_size) }
+ let(:schema) { double('Schema', default_max_page_size: 3) }
+
+ let_it_be(:caching_resolver) do
+ mod = described_class
+
+ Class.new(::Resolvers::BaseResolver) do
+ include mod
+
+ def query_input(is_admin:)
+ is_admin
+ end
+
+ def query_for(is_admin)
+ if is_admin.nil?
+ model_class.all
+ else
+ model_class.where(admin: is_admin)
+ end
+ end
+
+ def model_class
+ User # Happens to include FromUnion, and is cheap-ish to create
+ end
+ end
+ end
+
+ describe '#resolve' do
+ context 'there are more than MAX_UNION_SIZE queries' do
+ let_it_be(:max_union) { 3 }
+ let_it_be(:resolver) do
+ mod = described_class
+ max = max_union
+
+ Class.new(::Resolvers::BaseResolver) do
+ include mod
+
+ def query_input(username:)
+ username
+ end
+
+ def query_for(username)
+ if username.nil?
+ model_class.all
+ else
+ model_class.where(username: username)
+ end
+ end
+
+ def model_class
+ User # Happens to include FromUnion, and is cheap-ish to create
+ end
+
+ define_method :max_union_size do
+ max
+ end
+ end
+ end
+
+ it 'executes the queries in multiple batches' do
+ users = create_list(:user, (max_union * 2) + 1)
+ expect(User).to receive(:from_union).twice.and_call_original
+
+ results = users.in_groups_of(2, false).map do |users|
+ resolve(resolver, args: { username: users.map(&:username) }, field: field, schema: schema)
+ end
+
+ expect(results.flat_map(&method(:force))).to match_array(users)
+ end
+ end
+
+ context 'all queries return results' do
+ let_it_be(:admins) { create_list(:admin, 3) }
+
+ it 'batches the queries' do
+ expect do
+ [resolve_users(true), resolve_users(false)].each(&method(:force))
+ end.to issue_same_number_of_queries_as { force(resolve_users(nil)) }
+ end
+
+ it 'finds the correct values' do
+ found_admins = resolve_users(true)
+ found_others = resolve_users(false)
+ admins_again = resolve_users(true)
+ found_all = resolve_users(nil)
+
+ expect(force(found_admins)).to match_array(admins)
+ expect(force(found_others)).to match_array(non_admins)
+ expect(force(admins_again)).to match_array(admins)
+ expect(force(found_all)).to match_array(admins + non_admins)
+ end
+ end
+
+ it 'does not perform a union of a query with itself' do
+ expect(User).to receive(:where).once.and_call_original
+
+ [resolve_users(false), resolve_users(false)].each(&method(:force))
+ end
+
+ context 'one of the queries returns no results' do
+ it 'finds the correct values' do
+ found_admins = resolve_users(true)
+ found_others = resolve_users(false)
+ found_all = resolve_users(nil)
+
+ expect(force(found_admins)).to be_empty
+ expect(force(found_others)).to match_array(non_admins)
+ expect(force(found_all)).to match_array(non_admins)
+ end
+ end
+
+ context 'one of the queries has already been cached' do
+ before do
+ force(resolve_users(nil))
+ end
+
+ it 'avoids further queries' do
+ expect do
+ repeated_find = resolve_users(nil)
+
+ expect(force(repeated_find)).to match_array(non_admins)
+ end.not_to exceed_query_limit(0)
+ end
+ end
+
+ context 'the resolver overrides item_found' do
+ let_it_be(:admins) { create_list(:admin, 2) }
+ let(:query_context) do
+ {
+ found: { true => [], false => [], nil => [] }
+ }
+ end
+
+ let_it_be(:with_item_found) do
+ Class.new(caching_resolver) do
+ def item_found(key, item)
+ context[:found][key] << item
+ end
+ end
+ end
+
+ it 'receives item_found for each key the item mapped to' do
+ found_admins = resolve_users(true, with_item_found)
+ found_all = resolve_users(nil, with_item_found)
+
+ [found_admins, found_all].each(&method(:force))
+
+ expect(query_context[:found]).to match({
+ false => be_empty,
+ true => match_array(admins),
+ nil => match_array(admins + non_admins)
+ })
+ end
+ end
+
+ context 'the max_page_size is lower than the total result size' do
+ let(:max_page_size) { 2 }
+
+ it 'respects the max_page_size, on a per subset basis' do
+ found_all = resolve_users(nil)
+ found_others = resolve_users(false)
+
+ expect(force(found_all).size).to eq(2)
+ expect(force(found_others).size).to eq(2)
+ end
+ end
+
+ context 'the field does not declare max_page_size' do
+ let(:max_page_size) { nil }
+
+ it 'takes the page size from schema.default_max_page_size' do
+ found_all = resolve_users(nil)
+ found_others = resolve_users(false)
+
+ expect(force(found_all).size).to eq(schema.default_max_page_size)
+ expect(force(found_others).size).to eq(schema.default_max_page_size)
+ end
+ end
+
+ specify 'force . resolve === to_a . query_for . query_input' do
+ r = resolver_instance(caching_resolver)
+ args = { is_admin: false }
+
+ naive = r.query_for(r.query_input(**args)).to_a
+
+ expect(force(r.resolve(**args))).to eq(naive)
+ end
+ end
+
+ def resolve_users(is_admin, resolver = caching_resolver)
+ args = { is_admin: is_admin }
+ resolve(resolver, args: args, field: field, ctx: query_context, schema: schema)
+ end
+
+ def force(lazy)
+ ::Gitlab::Graphql::Lazy.force(lazy)
+ end
+end