diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-18 09:11:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-18 09:11:52 +0300 |
commit | 4d16568658ac6fb0003b407e07a76c11e607f44f (patch) | |
tree | 9084e7660f101d2cd70568f293257678ac5f2ef5 /spec/graphql | |
parent | f5410eefec8642bed6e7e3051319c52d7cbfb101 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/graphql')
8 files changed, 586 insertions, 61 deletions
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb index 33b11e1ca09..7ab5adadd1f 100644 --- a/spec/graphql/features/authorization_spec.rb +++ b/spec/graphql/features/authorization_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Gitlab::Graphql::Authorize' do +RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do include GraphqlHelpers include Graphql::ResolverFactories @@ -10,10 +10,14 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:permission_single) { :foo } let(:permission_collection) { [:foo, :bar] } let(:test_object) { double(name: 'My name') } + let(:authorizing_object) { test_object } + # to override when combining permissions + let(:permission_object_one) { authorizing_object } + let(:permission_object_two) { authorizing_object } + let(:query_string) { '{ item { name } }' } let(:result) do schema = empty_schema - schema.use(Gitlab::Graphql::Authorize) execute_query(query_type, schema: schema) end @@ -33,18 +37,25 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do shared_examples 'authorization with a collection of permissions' do it 'returns the protected field when user has all permissions' do - permit(*permission_collection) + permit_on(permission_object_one, permission_collection.first) + permit_on(permission_object_two, permission_collection.second) expect(subject).to eq('name' => test_object.name) end it 'returns nil when user only has one of the permissions' do - permit(permission_collection.first) + permit_on(permission_object_one, permission_collection.first) + + expect(subject).to be_nil + end + + it 'returns nil when user only has the other of the permissions' do + permit_on(permission_object_two, permission_collection.second) expect(subject).to be_nil end - it 'returns nil when user only has none of the permissions' do + it 'returns nil when user has neither of the required permissions' do expect(subject).to be_nil end end @@ -56,6 +67,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do describe 'Field authorizations' do let(:type) { type_factory } + let(:authorizing_object) { nil } describe 'with a single permission' do let(:query_type) do @@ -71,9 +83,10 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:query_type) do permissions = permission_collection query_factory do |qt| - qt.field :item, type, null: true, resolver: new_resolver(test_object) do - authorize permissions - end + qt.field :item, type, + null: true, + resolver: new_resolver(test_object), + authorize: permissions end end @@ -110,9 +123,8 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do let(:type) do permissions = permission_collection type_factory do |type| - type.field :name, GraphQL::STRING_TYPE, null: true do - authorize permissions - end + type.field :name, GraphQL::STRING_TYPE, null: true, + authorize: permissions end end @@ -163,6 +175,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do end describe 'type and field authorizations together' do + let(:authorizing_object) { anything } let(:permission_1) { permission_collection.first } let(:permission_2) { permission_collection.last } @@ -181,7 +194,62 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do include_examples 'authorization with a collection of permissions' end - describe 'type authorizations when applied to a relay connection' do + describe 'resolver and field authorizations together' do + let(:permission_1) { permission_collection.first } + let(:permission_2) { permission_collection.last } + let(:type) { type_factory } + + let(:query_type) do + query_factory do |query| + query.field :item, type, null: true, + resolver: resolver, + authorize: permission_2 + end + end + + context 'when the resolver authorizes the object' do + let(:permission_object_one) { be_nil } + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = simple_resolver(test_object) + resolver.include(::Gitlab::Graphql::Authorize::AuthorizeResource) + resolver.authorize permission_1 + resolver.authorizes_object! + resolver + end + + include_examples 'authorization with a collection of permissions' + end + + context 'when the resolver does not authorize the object, but instead calls authorized_find!' do + let(:permission_object_one) { test_object } + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = new_resolver(test_object, method: :find_object) + resolver.authorize permission_1 + resolver + end + + include_examples 'authorization with a collection of permissions' + end + + context 'when the resolver calls authorized_find!, but does not list any permissions' do + let(:permission_object_two) { be_nil } + let(:resolver) do + resolver = new_resolver(test_object, method: :find_object) + resolver + end + + it 'raises a configuration error' do + permit_on(permission_object_two, permission_collection.second) + + expect { execute_query(query_type) } + .to raise_error(::Gitlab::Graphql::Authorize::AuthorizeResource::ConfigurationError) + end + end + end + + describe 'when type authorizations when applied to a relay connection' do let(:query_string) { '{ item { edges { node { name } } } }' } let(:second_test_object) { double(name: 'Second thing') } @@ -303,8 +371,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do private def permit(*permissions) + permit_on(authorizing_object, *permissions) + end + + def permit_on(object, *permissions) permissions.each do |permission| - allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true) + allow(Ability).to receive(:allowed?).with(user, permission, object).and_return(true) end end end diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index cb2bb25b098..4ddd74b1eb7 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -14,10 +14,6 @@ RSpec.describe GitlabSchema do expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::GenericTracing)) end - it 'enables the authorization instrumenter' do - expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation)) - end - it 'has the base mutation' do expect(described_class.mutation).to eq(::Types::MutationType) end diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb index 24104a20465..dd9305d2197 100644 --- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb +++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Mutations::Boards::Issues::IssueMoveList do + include GraphqlHelpers + let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, group: group) } let_it_be(:board) { create(:board, group: group) } @@ -16,9 +18,8 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) } let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) } - let(:current_user) { user } - let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } - let(:params) { { board: board, project_path: project.full_path, iid: issue1.iid } } + let(:current_ctx) { { current_user: user } } + let(:params) { { board_id: global_id_of(board), project_path: project.full_path, iid: issue1.iid } } let(:move_params) do { from_list_id: list1.id, @@ -33,26 +34,45 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do group.add_guest(guest) end - subject do - mutation.resolve(**params.merge(move_params)) - end + describe '#resolve' do + subject do + sync(resolve(described_class, args: params.merge(move_params), ctx: current_ctx)) + end + + %i[from_list_id to_list_id].each do |arg_name| + context "when we only pass #{arg_name}" do + let(:move_params) { { arg_name => list1.id } } - describe '#ready?' do - it 'raises an error if required arguments are missing' do - expect { mutation.ready?(**params) } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \ - "fromListId, toListId, afterId or beforeId is required") + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ArgumentError, + 'Both fromListId and toListId must be present' + ) + end + end end - it 'raises an error if only one of fromListId and toListId is present' do - expect { mutation.ready?(**params.merge(from_list_id: list1.id)) } - .to raise_error(Gitlab::Graphql::Errors::ArgumentError, - 'Both fromListId and toListId must be present' + context 'when required arguments are missing' do + let(:move_params) { {} } + + it 'raises an error' do + expect { subject }.to raise_error( + Gitlab::Graphql::Errors::ArgumentError, + "At least one of the arguments fromListId, toListId, afterId or beforeId is required" ) + end + end + + context 'when the board ID is wrong' do + before do + params[:board_id] = global_id_of(project) + end + + it 'raises an error' do + expect { subject }.to raise_error(::GraphQL::LoadApplicationObjectFailedError) + end end - end - describe '#resolve' do context 'when user have access to resources' do it 'moves and repositions issue' do subject @@ -63,15 +83,11 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do end end - context 'when user have no access to resources' do - shared_examples 'raises a resource not available error' do - it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) } - end - - context 'when user cannot update issue' do - let(:current_user) { guest } + context 'when user cannot update issue' do + let(:current_ctx) { { current_user: guest } } - it_behaves_like 'raises a resource not available error' + specify do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end end diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb index 326d88cea80..e92000194b1 100644 --- a/spec/graphql/mutations/design_management/upload_spec.rb +++ b/spec/graphql/mutations/design_management/upload_spec.rb @@ -32,6 +32,10 @@ RSpec.describe Mutations::DesignManagement::Upload do end context "when the feature is not available" do + before do + enable_design_management(false) + end + it_behaves_like "resource not available" end @@ -99,20 +103,20 @@ RSpec.describe Mutations::DesignManagement::Upload do it_behaves_like "resource not available" end - context "a valid design" do + context "with a valid design" do it "returns the updated designs" do expect(resolve[:errors]).to eq [] expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png") end end - context "context when passing an invalid project" do + context "when passing an invalid project" do let(:project) { build(:project) } it_behaves_like "resource not available" end - context "context when passing an invalid issue" do + context "when passing an invalid issue" do let(:issue) { build(:issue) } it_behaves_like "resource not available" diff --git a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb index 27ac1572cab..4c244da5c62 100644 --- a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb +++ b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb @@ -38,11 +38,8 @@ RSpec.describe LooksAhead do user = Class.new(GraphQL::Schema::Object) do graphql_name 'User' field :name, String, null: true - field :issues, issue.connection_type, - null: true - field :issues_with_lookahead, issue.connection_type, - resolver: issues_resolver, - null: true + field :issues, issue.connection_type, null: true + field :issues_with_lookahead, issue.connection_type, resolver: issues_resolver, null: true end Class.new(GraphQL::Schema) do @@ -101,7 +98,7 @@ RSpec.describe LooksAhead do expect(res['errors']).to be_blank expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name) - %w(issues issuesWithLookahead).each do |field| + %w[issues issuesWithLookahead].each do |field| expect(all_issue_titles(res, field)).to match_array(issue_titles) expect(all_label_ids(res, field)).to match_array(expected_label_ids) end diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 7dd968d90a8..5e2a075c5b3 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do # AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC # SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2 # SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2 - let(:queries_per_project) { 3 } + let(:queries_per_project) { 4 } context 'no arguments' do it 'returns all merge requests' do diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb index b10c2a2ab2a..7e4110af9a4 100644 --- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb +++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb @@ -48,15 +48,21 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do end end - context 'without project' do - let_it_be(:integration) { create(:prometheus_service, project: nil, group: create(:group)) } - - it_behaves_like 'has field with value', 'token' do - let(:value) { nil } - end - - it_behaves_like 'has field with value', 'url' do - let(:value) { nil } + context 'group integration' do + let_it_be(:group) { create(:group) } + let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) } + + # Since it is impossible to authorize the parent here, given that the + # project is nil, all fields should be redacted: + + described_class.fields.keys.each do |field_name| + context "field: #{field_name}" do + it 'is redacted' do + expect do + resolve_field(field_name, integration, current_user: user) + end.to raise_error(GraphqlHelpers::UnauthorizedObject) + end + end end end end diff --git a/spec/graphql/types/base_object_spec.rb b/spec/graphql/types/base_object_spec.rb new file mode 100644 index 00000000000..d144c1f6456 --- /dev/null +++ b/spec/graphql/types/base_object_spec.rb @@ -0,0 +1,434 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::BaseObject do + include GraphqlHelpers + + describe 'scoping items' do + let_it_be(:custom_auth) do + Class.new(::Gitlab::Graphql::Authorize::ObjectAuthorization) do + def any? + true + end + + def ok?(object, _current_user) + return false if object == { id: 100 } + return false if object.try(:deactivated?) + + true + end + end + end + + let_it_be(:test_schema) do + auth = custom_auth.new(nil) + + base_object = Class.new(described_class) do + # Override authorization so we don't need to mock Ability + define_singleton_method :authorization do + auth + end + end + + y_type = Class.new(base_object) do + graphql_name 'Y' + authorize :read_y + field :id, Integer, null: false + + def id + object[:id] + end + end + + number_type = Module.new do + include ::Types::BaseInterface + + graphql_name 'Number' + + field :value, Integer, null: false + end + + odd_type = Class.new(described_class) do + graphql_name 'Odd' + implements number_type + + authorize :read_odd + field :odd_value, Integer, null: false + + def odd_value + object[:value] + end + end + + even_type = Class.new(described_class) do + graphql_name 'Even' + implements number_type + + authorize :read_even + field :even_value, Integer, null: false + + def even_value + object[:value] + end + end + + # an abstract type, delegating authorization to members + odd_or_even = Class.new(::Types::BaseUnion) do + graphql_name 'OddOrEven' + + possible_types odd_type, even_type + + define_singleton_method :resolve_type do |object, ctx| + if object[:value].odd? + odd_type + else + even_type + end + end + end + + number_type.define_singleton_method :resolve_type do |object, ctx| + odd_or_even.resolve_type(object, ctx) + end + + x_type = Class.new(base_object) do + graphql_name 'X' + # Scalar types + field :title, String, null: true + # monomorphic types + field :lazy_list_of_ys, [y_type], null: true + field :list_of_lazy_ys, [y_type], null: true + field :array_ys_conn, y_type.connection_type, null: true + # polymorphic types + field :polymorphic_conn, odd_or_even.connection_type, null: true + field :polymorphic_object, odd_or_even, null: true do + argument :value, Integer, required: true + end + field :interface_conn, number_type.connection_type, null: true + + def lazy_list_of_ys + ::Gitlab::Graphql::Lazy.new { object[:ys] } + end + + def list_of_lazy_ys + object[:ys].map { |y| ::Gitlab::Graphql::Lazy.new { y } } + end + + def array_ys_conn + object[:ys].dup + end + + def polymorphic_conn + object[:values].dup + end + alias_method :interface_conn, :polymorphic_conn + + def polymorphic_object(value) + value + end + end + + user_type = Class.new(base_object) do + graphql_name 'User' + authorize :read_user + field 'name', String, null: true + end + + Class.new(GraphQL::Schema) do + lazy_resolve ::Gitlab::Graphql::Lazy, :force + use ::GraphQL::Pagination::Connections + use ::Gitlab::Graphql::Pagination::Connections + + query(Class.new(::Types::BaseObject) do + graphql_name 'Query' + field :x, x_type, null: true + field :users, user_type.connection_type, null: true + + def x + ::Gitlab::Graphql::Lazy.new { context[:x] } + end + + def users + ::Gitlab::Graphql::Lazy.new { User.id_in(context[:user_ids]).order(id: :asc) } + end + end) + + def unauthorized_object(err) + nil + end + end + end + + def document(path) + GraphQL.parse(<<~GQL) + query { + x { + title + #{query_graphql_path(path, 'id')} + } + } + GQL + end + + let(:data) do + { + x: { + title: 'Hey', + ys: [{ id: 1 }, { id: 100 }, { id: 2 }] + } + } + end + + shared_examples 'array member redaction' do |path| + let(:result) do + query = GraphQL::Query.new(test_schema, document: document(path), context: data) + query.result.to_h + end + + it 'redacts the unauthorized array member' do + expect(graphql_dig_at(result, 'data', 'x', 'title')).to eq('Hey') + expect(graphql_dig_at(result, 'data', 'x', *path)).to contain_exactly( + eq({ 'id' => 1 }), + eq({ 'id' => 2 }) + ) + end + end + + # For example a batchloaded association + context 'a lazy list' do + it_behaves_like 'array member redaction', %w[lazyListOfYs] + end + + # For example using a batchloader to map over a set of IDs + context 'a list of lazy items' do + it_behaves_like 'array member redaction', %w[listOfLazyYs] + end + + context 'an array connection of items' do + it_behaves_like 'array member redaction', %w[arrayYsConn nodes] + end + + context 'an array connection of items, selecting edges' do + it_behaves_like 'array member redaction', %w[arrayYsConn edges node] + end + + it 'paginates arrays correctly' do + n = 7 + + data = { + x: { + ys: (95..105).to_a.map { |id| { id: id } } + } + } + + doc = ->(after) do + GraphQL.parse(<<~GQL) + query { + x { + ys: arrayYsConn(#{attributes_to_graphql(first: n, after: after)}) { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + } + nodes { id } + } + } + } + GQL + end + returned_items = ->(ids) do + ids.to_a.map { |id| eq({ 'id' => id }) } + end + + query = GraphQL::Query.new(test_schema, document: doc[nil], context: data) + result = query.result.to_h + + ys = result.dig('data', 'x', 'ys', 'nodes') + page = result.dig('data', 'x', 'ys', 'pageInfo') + # We expect this page to be smaller, since we paginate before redaction + expect(ys).to match_array(returned_items[(95..101).to_a - [100]]) + expect(page).to include('hasNextPage' => true, 'hasPreviousPage' => false) + + cursor = page['endCursor'] + query_2 = GraphQL::Query.new(test_schema, document: doc[cursor], context: data) + result_2 = query_2.result.to_h + + ys = result_2.dig('data', 'x', 'ys', 'nodes') + page = result_2.dig('data', 'x', 'ys', 'pageInfo') + expect(ys).to match_array(returned_items[102..105]) + expect(page).to include('hasNextPage' => false, 'hasPreviousPage' => true) + end + + it 'filters connections correctly' do + active_users = create_list(:user, 3, state: :active) + inactive = create(:user, state: :deactivated) + + data = { user_ids: [inactive, *active_users].map(&:id) } + + doc = GraphQL.parse(<<~GQL) + query { + users { nodes { name } } + } + GQL + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + expect(result.dig('data', 'users', 'nodes')).to match_array(active_users.map do |u| + eq({ 'name' => u.name }) + end) + end + + it 'filters polymorphic connections' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + things: polymorphicConn { + nodes { + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_even, { value: 2 }).and_return(false) + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_even, { value: 4 }).and_return(true) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + things = result.dig('data', 'x', 'things', 'nodes') + + expect(things).to contain_exactly( + { 'oddValue' => 1 }, + { 'evenValue' => 4 } + ) + end + + it 'filters interface connections' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + things: interfaceConn { + nodes { + value + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_even, { value: 2 }).and_return(false) + expect(Ability).to receive(:allowed?).twice + .with(:the_user, :read_even, { value: 4 }).and_return(true) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + things = result.dig('data', 'x', 'things', 'nodes') + + expect(things).to contain_exactly( + { 'value' => 1, 'oddValue' => 1 }, + { 'value' => 4, 'evenValue' => 4 } + ) + end + + it 'redacts polymorphic objects' do + data = { + current_user: :the_user, + x: { + values: [{ value: 1 }] + } + } + + doc = GraphQL.parse(<<~GQL) + query { + x { + ok: polymorphicObject(value: 1) { + ... on Odd { oddValue } + ... on Even { evenValue } + } + bad: polymorphicObject(value: 3) { + ... on Odd { oddValue } + ... on Even { evenValue } + } + } + } + GQL + + # Each ability check happens twice: once in the collection, and once + # on the type. We expect the ability checks to be cached. + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 1 }).and_return(true) + expect(Ability).to receive(:allowed?).once + .with(:the_user, :read_odd, { value: 3 }).and_return(false) + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + expect(result.dig('data', 'x', 'ok')).to eq({ 'oddValue' => 1 }) + expect(result.dig('data', 'x', 'bad')).to be_nil + end + + it 'paginates before scoping' do + # Inactive first so they sort first + n = 3 + inactive = create_list(:user, n - 1, state: :deactivated) + active_users = create_list(:user, 2, state: :active) + + data = { user_ids: [*inactive, *active_users].map(&:id) } + + doc = GraphQL.parse(<<~GQL) + query { + users(first: #{n}) { + pageInfo { hasNextPage } + nodes { name } } + } + GQL + + query = GraphQL::Query.new(test_schema, document: doc, context: data) + result = query.result.to_h + + # We expect the page to be loaded and then filtered - i.e. to have all + # deactivated users removed. + expect(result.dig('data', 'users', 'pageInfo', 'hasNextPage')).to be_truthy + expect(result.dig('data', 'users', 'nodes')) + .to contain_exactly({ 'name' => active_users.first.name }) + end + end +end |