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:
Diffstat (limited to 'spec/graphql/types/base_object_spec.rb')
-rw-r--r--spec/graphql/types/base_object_spec.rb432
1 files changed, 432 insertions, 0 deletions
diff --git a/spec/graphql/types/base_object_spec.rb b/spec/graphql/types/base_object_spec.rb
new file mode 100644
index 00000000000..d8f2ef58ea5
--- /dev/null
+++ b/spec/graphql/types/base_object_spec.rb
@@ -0,0 +1,432 @@
+# 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
+ describe '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
+ describe 'a list of lazy items' do
+ it_behaves_like 'array member redaction', %w[listOfLazyYs]
+ end
+
+ describe 'an array connection of items' do
+ it_behaves_like 'array member redaction', %w[arrayYsConn nodes]
+ end
+
+ describe '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 = lambda do |after|
+ 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) { ids.to_a.map { |id| eq({ 'id' => id }) } }
+
+ 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