diff options
Diffstat (limited to 'spec/graphql/types/base_object_spec.rb')
-rw-r--r-- | spec/graphql/types/base_object_spec.rb | 432 |
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 |