Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-18 09:11:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-18 09:11:52 +0300
commit4d16568658ac6fb0003b407e07a76c11e607f44f (patch)
tree9084e7660f101d2cd70568f293257678ac5f2ef5 /spec/graphql
parentf5410eefec8642bed6e7e3051319c52d7cbfb101 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/graphql')
-rw-r--r--spec/graphql/features/authorization_spec.rb98
-rw-r--r--spec/graphql/gitlab_schema_spec.rb4
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb66
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb10
-rw-r--r--spec/graphql/resolvers/concerns/looks_ahead_spec.rb9
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb2
-rw-r--r--spec/graphql/types/alert_management/prometheus_integration_type_spec.rb24
-rw-r--r--spec/graphql/types/base_object_spec.rb434
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