summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/graphql
diff options
context:
space:
mode:
Diffstat (limited to 'spec/lib/gitlab/graphql')
-rw-r--r--spec/lib/gitlab/graphql/batch_key_spec.rb78
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb41
-rw-r--r--spec/lib/gitlab/graphql/queries_spec.rb343
3 files changed, 462 insertions, 0 deletions
diff --git a/spec/lib/gitlab/graphql/batch_key_spec.rb b/spec/lib/gitlab/graphql/batch_key_spec.rb
new file mode 100644
index 00000000000..881fba5c1be
--- /dev/null
+++ b/spec/lib/gitlab/graphql/batch_key_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'test_prof/recipes/rspec/let_it_be'
+
+RSpec.describe ::Gitlab::Graphql::BatchKey do
+ let_it_be(:rect) { Struct.new(:len, :width) }
+ let_it_be(:circle) { Struct.new(:radius) }
+ let(:lookahead) { nil }
+ let(:object) { rect.new(2, 3) }
+
+ subject { described_class.new(object, lookahead, object_name: :rect) }
+
+ it 'is equal to keys of the same object, regardless of lookahead or object name' do
+ expect(subject).to eq(described_class.new(rect.new(2, 3)))
+ expect(subject).to eq(described_class.new(rect.new(2, 3), :anything))
+ expect(subject).to eq(described_class.new(rect.new(2, 3), lookahead, object_name: :does_not_matter))
+ expect(subject).not_to eq(described_class.new(rect.new(2, 4)))
+ expect(subject).not_to eq(described_class.new(circle.new(10)))
+ end
+
+ it 'delegates attribute lookup methods to the inner object' do
+ other = rect.new(2, 3)
+
+ expect(subject.hash).to eq(other.hash)
+ expect(subject.len).to eq(other.len)
+ expect(subject.width).to eq(other.width)
+ end
+
+ it 'allows the object to be named more meaningfully' do
+ expect(subject.object).to eq(object)
+ expect(subject.object).to eq(subject.rect)
+ end
+
+ it 'works as a hash key' do
+ h = { subject => :foo }
+
+ expect(h[described_class.new(object)]).to eq(:foo)
+ end
+
+ describe '#requires?' do
+ it 'returns false if the lookahead was not provided' do
+ expect(subject.requires?([:foo])).to be(false)
+ end
+
+ context 'lookahead was provided' do
+ let(:lookahead) { double(:Lookahead) }
+
+ before do
+ allow(lookahead).to receive(:selection).with(Symbol).and_return(lookahead)
+ end
+
+ it 'returns false if the path is empty' do
+ expect(subject.requires?([])).to be(false)
+ end
+
+ context 'it selects the field' do
+ before do
+ allow(lookahead).to receive(:selects?).with(Symbol).once.and_return(true)
+ end
+
+ it 'returns true' do
+ expect(subject.requires?(%i[foo bar baz])).to be(true)
+ end
+ end
+
+ context 'it does not select the field' do
+ before do
+ allow(lookahead).to receive(:selects?).with(Symbol).once.and_return(false)
+ end
+
+ it 'returns false' do
+ expect(subject.requires?(%i[foo bar baz])).to be(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index 0ac54a20fcc..02e67488d3f 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -21,6 +21,47 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/297358
+ context 'the relation has been preloaded' do
+ let(:projects) { Project.all.preload(:issues) }
+ let(:nodes) { projects.first.issues }
+
+ before do
+ project = create(:project)
+ create_list(:issue, 3, project: project)
+ end
+
+ it 'is loaded' do
+ expect(nodes).to be_loaded
+ end
+
+ it 'does not error when accessing pagination information' do
+ connection.first = 2
+
+ expect(connection).to have_attributes(
+ has_previous_page: false,
+ has_next_page: true
+ )
+ end
+
+ it 'can generate cursors' do
+ connection.send(:ordered_items) # necessary to generate the order-list
+
+ expect(connection.cursor_for(nodes.first)).to be_a(String)
+ end
+
+ it 'can read the next page' do
+ connection.send(:ordered_items) # necessary to generate the order-list
+ ordered = nodes.reorder(id: :desc)
+ next_page = described_class.new(nodes,
+ context: context,
+ max_page_size: 3,
+ after: connection.cursor_for(ordered.second))
+
+ expect(next_page.sliced_nodes).to contain_exactly(ordered.third)
+ end
+ end
+
it_behaves_like 'a connection with collection methods'
it_behaves_like 'a redactable connection' do
diff --git a/spec/lib/gitlab/graphql/queries_spec.rb b/spec/lib/gitlab/graphql/queries_spec.rb
new file mode 100644
index 00000000000..6e08a87523f
--- /dev/null
+++ b/spec/lib/gitlab/graphql/queries_spec.rb
@@ -0,0 +1,343 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require "test_prof/recipes/rspec/let_it_be"
+
+RSpec.describe Gitlab::Graphql::Queries do
+ shared_examples 'a valid GraphQL query for the blog schema' do
+ it 'is valid' do
+ expect(subject.validate(schema).second).to be_empty
+ end
+ end
+
+ shared_examples 'an invalid GraphQL query for the blog schema' do
+ it 'is invalid' do
+ expect(subject.validate(schema).second).to match errors
+ end
+ end
+
+ # Toy schema to validate queries against
+ let_it_be(:schema) do
+ author = Class.new(GraphQL::Schema::Object) do
+ graphql_name 'Author'
+ field :name, GraphQL::STRING_TYPE, null: true
+ field :handle, GraphQL::STRING_TYPE, null: false
+ field :verified, GraphQL::BOOLEAN_TYPE, null: false
+ end
+
+ post = Class.new(GraphQL::Schema::Object) do
+ graphql_name 'Post'
+ field :name, GraphQL::STRING_TYPE, null: false
+ field :title, GraphQL::STRING_TYPE, null: false
+ field :content, GraphQL::STRING_TYPE, null: true
+ field :author, author, null: false
+ end
+ author.field :posts, [post], null: false do
+ argument :blog_title, GraphQL::STRING_TYPE, required: false
+ end
+
+ blog = Class.new(GraphQL::Schema::Object) do
+ graphql_name 'Blog'
+ field :title, GraphQL::STRING_TYPE, null: false
+ field :description, GraphQL::STRING_TYPE, null: false
+ field :main_author, author, null: false
+ field :posts, [post], null: false
+ field :post, post, null: true do
+ argument :slug, GraphQL::STRING_TYPE, required: true
+ end
+ end
+
+ Class.new(GraphQL::Schema) do
+ query(Class.new(GraphQL::Schema::Object) do
+ graphql_name 'Query'
+ field :blog, blog, null: true do
+ argument :title, GraphQL::STRING_TYPE, required: true
+ end
+ field :post, post, null: true do
+ argument :slug, GraphQL::STRING_TYPE, required: true
+ end
+ end)
+ end
+ end
+
+ let(:root) do
+ Rails.root / 'fixtures/lib/gitlab/graphql/queries'
+ end
+
+ describe Gitlab::Graphql::Queries::Fragments do
+ subject { described_class.new(root) }
+
+ it 'has the right home' do
+ expect(subject.home).to eq (root / 'app/assets/javascripts').to_s
+ end
+
+ it 'has the right EE home' do
+ expect(subject.home_ee).to eq (root / 'ee/app/assets/javascripts').to_s
+ end
+
+ it 'caches query definitions' do
+ fragment = subject.get('foo')
+
+ expect(fragment).to be_a(::Gitlab::Graphql::Queries::Definition)
+ expect(subject.get('foo')).to be fragment
+ end
+ end
+
+ describe '.all' do
+ it 'is the combination of finding queries in CE and EE' do
+ expect(described_class)
+ .to receive(:find).with(Rails.root / 'app/assets/javascripts').and_return([:ce])
+ expect(described_class)
+ .to receive(:find).with(Rails.root / 'ee/app/assets/javascripts').and_return([:ee])
+
+ expect(described_class.all).to eq([:ce, :ee])
+ end
+ end
+
+ describe '.find' do
+ def definition_of(path)
+ be_a(::Gitlab::Graphql::Queries::Definition)
+ .and(have_attributes(file: path.to_s))
+ end
+
+ it 'find a single specific file' do
+ path = root / 'post_by_slug.graphql'
+
+ expect(described_class.find(path)).to contain_exactly(definition_of(path))
+ end
+
+ it 'ignores files that do not exist' do
+ path = root / 'not_there.graphql'
+
+ expect(described_class.find(path)).to be_empty
+ end
+
+ it 'ignores fragments' do
+ path = root / 'author.fragment.graphql'
+
+ expect(described_class.find(path)).to be_empty
+ end
+
+ it 'ignores typedefs' do
+ path = root / 'typedefs.graphql'
+
+ expect(described_class.find(path)).to be_empty
+ end
+
+ it 'finds all query definitions under a root directory' do
+ found = described_class.find(root)
+
+ expect(found).to include(
+ definition_of(root / 'post_by_slug.graphql'),
+ definition_of(root / 'post_by_slug.with_import.graphql'),
+ definition_of(root / 'post_by_slug.with_import.misspelled.graphql'),
+ definition_of(root / 'duplicate_imports.graphql'),
+ definition_of(root / 'deeply/nested/query.graphql')
+ )
+
+ expect(found).not_to include(
+ definition_of(root / 'typedefs.graphql'),
+ definition_of(root / 'author.fragment.graphql')
+ )
+ end
+ end
+
+ describe Gitlab::Graphql::Queries::Definition do
+ let(:fragments) { Gitlab::Graphql::Queries::Fragments.new(root, '.') }
+
+ subject { described_class.new(root / path, fragments) }
+
+ context 'a simple query' do
+ let(:path) { 'post_by_slug.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a query with an import' do
+ let(:path) { 'post_by_slug.with_import.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a query with duplicate imports' do
+ let(:path) { 'duplicate_imports.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a query importing from ee_else_ce' do
+ let(:path) { 'ee_else_ce.import.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'can resolve the ee fields' do
+ expect(subject.text(mode: :ce)).not_to include('verified')
+ expect(subject.text(mode: :ee)).to include('verified')
+ end
+ end
+
+ context 'a query refering to parent directories' do
+ let(:path) { 'deeply/nested/query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a query refering to parent directories, incorrectly' do
+ let(:path) { 'deeply/nested/bad_import.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ be_a(::Gitlab::Graphql::Queries::FileNotFound)
+ .and(have_attributes(message: include('deeply/author.fragment.graphql')))
+ )
+ end
+ end
+ end
+
+ context 'a query with a broken import' do
+ let(:path) { 'post_by_slug.with_import.misspelled.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ be_a(::Gitlab::Graphql::Queries::FileNotFound)
+ .and(have_attributes(message: include('auther.fragment.graphql')))
+ )
+ end
+ end
+ end
+
+ context 'a query which imports a file with a broken import' do
+ let(:path) { 'transitive_bad_import.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ be_a(::Gitlab::Graphql::Queries::FileNotFound)
+ .and(have_attributes(message: include('does-not-exist.graphql')))
+ )
+ end
+ end
+ end
+
+ context 'a query containing a client directive' do
+ let(:path) { 'client.query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'is tagged as a client query' do
+ expect(subject.validate(schema).first).to eq :client_query
+ end
+ end
+
+ context 'a mixed client query, valid' do
+ let(:path) { 'mixed_client.query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'is not tagged as a client query' do
+ expect(subject.validate(schema).first).not_to eq :client_query
+ end
+ end
+
+ context 'a mixed client query, with skipped argument' do
+ let(:path) { 'mixed_client_skipped_argument.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a mixed client query, with unused fragment' do
+ let(:path) { 'mixed_client_unused_fragment.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a client query, with unused fragment' do
+ let(:path) { 'client_unused_fragment.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+
+ it 'is tagged as a client query' do
+ expect(subject.validate(schema).first).to eq :client_query
+ end
+ end
+
+ context 'a mixed client query, invalid' do
+ let(:path) { 'mixed_client_invalid.query.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(have_attributes(message: include('titlz')))
+ end
+ end
+ end
+
+ context 'a query containing a connection directive' do
+ let(:path) { 'connection.query.graphql' }
+
+ it_behaves_like 'a valid GraphQL query for the blog schema'
+ end
+
+ context 'a query which mentions an incorrect field' do
+ let(:path) { 'wrong_field.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ have_attributes(message: /'createdAt' doesn't exist/),
+ have_attributes(message: /'categories' doesn't exist/)
+ )
+ end
+ end
+ end
+
+ context 'a query which has a missing argument' do
+ let(:path) { 'missing_argument.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ have_attributes(message: include('blog'))
+ )
+ end
+ end
+ end
+
+ context 'a query which has a bad argument' do
+ let(:path) { 'bad_argument.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ have_attributes(message: include('Nullability mismatch on variable $bad'))
+ )
+ end
+ end
+ end
+
+ context 'a query which has a syntax error' do
+ let(:path) { 'syntax-error.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ have_attributes(message: include('Parse error'))
+ )
+ end
+ end
+ end
+
+ context 'a query which has an unused import' do
+ let(:path) { 'unused_import.graphql' }
+
+ it_behaves_like 'an invalid GraphQL query for the blog schema' do
+ let(:errors) do
+ contain_exactly(
+ have_attributes(message: include('AuthorF was defined, but not used'))
+ )
+ end
+ end
+ end
+ end
+end