diff options
author | Douwe Maan <douwe@gitlab.com> | 2018-06-06 13:03:34 +0300 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2018-06-06 13:03:34 +0300 |
commit | af9cc234f2bf854de38e9730266a411f261918da (patch) | |
tree | bf25d7590d728ee1daa6a179ad43db87301475b8 /spec | |
parent | 7334b8556ba2ee06397293fd90ea04be8f3fccd2 (diff) | |
parent | 9b65d4bb417fb4939289eab94487c894f0a62db6 (diff) |
Merge branch 'bvl-graphql-start-34754' into 'master'
GraphQL setup: Basic Project and Merge request endpoint
Closes #34754
See merge request gitlab-org/gitlab-ce!19008
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/graphql_controller_spec.rb | 69 | ||||
-rw-r--r-- | spec/graphql/gitlab_schema_spec.rb | 33 | ||||
-rw-r--r-- | spec/graphql/resolvers/merge_request_resolver_spec.rb | 58 | ||||
-rw-r--r-- | spec/graphql/resolvers/project_resolver_spec.rb | 32 | ||||
-rw-r--r-- | spec/graphql/types/project_type_spec.rb | 5 | ||||
-rw-r--r-- | spec/graphql/types/query_type_spec.rb | 37 | ||||
-rw-r--r-- | spec/graphql/types/time_type_spec.rb | 16 | ||||
-rw-r--r-- | spec/lib/gitlab/path_regex_spec.rb | 12 | ||||
-rw-r--r-- | spec/requests/api/graphql/merge_request_query_spec.rb | 49 | ||||
-rw-r--r-- | spec/requests/api/graphql/project_query_spec.rb | 39 | ||||
-rw-r--r-- | spec/routing/api_routing_spec.rb | 31 | ||||
-rw-r--r-- | spec/support/helpers/graphql_helpers.rb | 90 | ||||
-rw-r--r-- | spec/support/matchers/graphql_matchers.rb | 40 | ||||
-rw-r--r-- | spec/support/shared_examples/requests/graphql_shared_examples.rb | 11 |
14 files changed, 517 insertions, 5 deletions
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 00000000000..1449036e148 --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe GraphqlController do + describe 'execute' do + let(:user) { nil } + + before do + sign_in(user) if user + + run_test_query! + end + + subject { query_response } + + context 'graphql is disabled by feature flag' do + let(:user) { nil } + + before do + stub_feature_flags(graphql: false) + end + + it 'returns 404' do + run_test_query! + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'signed out' do + let(:user) { nil } + + it 'runs the query with current_user: nil' do + is_expected.to eq('echo' => 'nil says: test success') + end + end + + context 'signed in' do + let(:user) { create(:user, username: 'Simon') } + + it 'runs the query with current_user set' do + is_expected.to eq('echo' => '"Simon" says: test success') + end + end + + context 'invalid variables' do + it 'returns an error' do + run_test_query!(variables: "This is not JSON") + + expect(response).to have_gitlab_http_status(422) + expect(json_response['errors'].first['message']).not_to be_nil + end + end + end + + # Chosen to exercise all the moving parts in GraphqlController#execute + def run_test_query!(variables: { 'text' => 'test success' }) + query = <<~QUERY + query Echo($text: String) { + echo(text: $text) + } + QUERY + + post :execute, query: query, operationName: 'Echo', variables: variables + end + + def query_response + json_response['data'] + end +end diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb new file mode 100644 index 00000000000..b892f6b44ed --- /dev/null +++ b/spec/graphql/gitlab_schema_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe GitlabSchema do + it 'uses batch loading' do + expect(field_instrumenters).to include(BatchLoader::GraphQL) + end + + it 'enables the preload instrumenter' do + expect(field_instrumenters).to include(BatchLoader::GraphQL) + end + + it 'enables the authorization instrumenter' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation)) + end + + it 'enables using presenters' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation)) + end + + it 'has the base mutation' do + pending('Adding an empty mutation breaks the documentation explorer') + + expect(described_class.mutation).to eq(::Types::MutationType.to_graphql) + end + + it 'has the base query' do + expect(described_class.query).to eq(::Types::QueryType.to_graphql) + end + + def field_instrumenters + described_class.instrumenters[:field] + end +end diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_request_resolver_spec.rb new file mode 100644 index 00000000000..af015533209 --- /dev/null +++ b/spec/graphql/resolvers/merge_request_resolver_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Resolvers::MergeRequestResolver do + include GraphqlHelpers + + set(:project) { create(:project, :repository) } + set(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) } + set(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) } + + set(:other_project) { create(:project, :repository) } + set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } + + let(:full_path) { project.full_path } + let(:iid_1) { merge_request_1.iid } + let(:iid_2) { merge_request_2.iid } + + let(:other_full_path) { other_project.full_path } + let(:other_iid) { other_merge_request.iid } + + describe '#resolve' do + it 'batch-resolves merge requests by target project full path and IID' do + path = full_path # avoid database query + + result = batch(max_queries: 2) do + [resolve_mr(path, iid_1), resolve_mr(path, iid_2)] + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'can batch-resolve merge requests from different projects' do + path = project.full_path # avoid database queries + other_path = other_full_path + + result = batch(max_queries: 3) do + [resolve_mr(path, iid_1), resolve_mr(path, iid_2), resolve_mr(other_path, other_iid)] + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) + end + + it 'resolves an unknown iid to nil' do + result = batch { resolve_mr(full_path, -1) } + + expect(result).to be_nil + end + + it 'resolves a known iid for an unknown full_path to nil' do + result = batch { resolve_mr('unknown/project', iid_1) } + + expect(result).to be_nil + end + end + + def resolve_mr(full_path, iid) + resolve(described_class, args: { full_path: full_path, iid: iid }) + end +end diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb new file mode 100644 index 00000000000..d4990c6492c --- /dev/null +++ b/spec/graphql/resolvers/project_resolver_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Resolvers::ProjectResolver do + include GraphqlHelpers + + set(:project1) { create(:project) } + set(:project2) { create(:project) } + + set(:other_project) { create(:project) } + + describe '#resolve' do + it 'batch-resolves projects by full path' do + paths = [project1.full_path, project2.full_path] + + result = batch(max_queries: 1) do + paths.map { |path| resolve_project(path) } + end + + expect(result).to contain_exactly(project1, project2) + end + + it 'resolves an unknown full_path to nil' do + result = batch { resolve_project('unknown/project') } + + expect(result).to be_nil + end + end + + def resolve_project(full_path) + resolve(described_class, args: { full_path: full_path }) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb new file mode 100644 index 00000000000..e0f89105b86 --- /dev/null +++ b/spec/graphql/types/project_type_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe GitlabSchema.types['Project'] do + it { expect(described_class.graphql_name).to eq('Project') } +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb new file mode 100644 index 00000000000..8488252fd59 --- /dev/null +++ b/spec/graphql/types/query_type_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe GitlabSchema.types['Query'] do + it 'is called Query' do + expect(described_class.graphql_name).to eq('Query') + end + + it { is_expected.to have_graphql_fields(:project, :merge_request, :echo) } + + describe 'project field' do + subject { described_class.fields['project'] } + + it 'finds projects by full path' do + is_expected.to have_graphql_arguments(:full_path) + is_expected.to have_graphql_type(Types::ProjectType) + is_expected.to have_graphql_resolver(Resolvers::ProjectResolver) + end + + it 'authorizes with read_project' do + is_expected.to require_graphql_authorizations(:read_project) + end + end + + describe 'merge_request field' do + subject { described_class.fields['mergeRequest'] } + + it 'finds MRs by project and IID' do + is_expected.to have_graphql_arguments(:full_path, :iid) + is_expected.to have_graphql_type(Types::MergeRequestType) + is_expected.to have_graphql_resolver(Resolvers::MergeRequestResolver) + end + + it 'authorizes with read_merge_request' do + is_expected.to require_graphql_authorizations(:read_merge_request) + end + end +end diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb new file mode 100644 index 00000000000..4196d9d27d4 --- /dev/null +++ b/spec/graphql/types/time_type_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe GitlabSchema.types['Time'] do + let(:iso) { "2018-06-04T15:23:50+02:00" } + let(:time) { Time.parse(iso) } + + it { expect(described_class.graphql_name).to eq('Time') } + + it 'coerces Time object into ISO 8601' do + expect(described_class.coerce_isolated_result(time)).to eq(iso) + end + + it 'coerces an ISO-time into Time object' do + expect(described_class.coerce_isolated_input(iso)).to eq(time) + end +end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index a40330d853f..e90e0aba0a4 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -90,11 +90,13 @@ describe Gitlab::PathRegex do let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } let(:top_level_words) do - words = routes_not_starting_in_wildcard.map do |route| - route.split('/')[1] - end.compact - - (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq + routes_not_starting_in_wildcard + .map { |route| route.split('/')[1] } + .concat(ee_top_level_words) + .concat(files_in_public) + .concat(Array(API::API.prefix.to_s)) + .compact + .uniq end let(:ee_top_level_words) do diff --git a/spec/requests/api/graphql/merge_request_query_spec.rb b/spec/requests/api/graphql/merge_request_query_spec.rb new file mode 100644 index 00000000000..12b1d5d18a2 --- /dev/null +++ b/spec/requests/api/graphql/merge_request_query_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'getting merge request information' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:current_user) { create(:user) } + + let(:query) do + attributes = { + 'fullPath' => merge_request.project.full_path, + 'iid' => merge_request.iid + } + graphql_query_for('mergeRequest', attributes) + end + + context 'when the user has access to the merge request' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + it 'returns the merge request' do + expect(graphql_data['mergeRequest']).not_to be_nil + end + + # This is a field coming from the `MergeRequestPresenter` + it 'includes a web_url' do + expect(graphql_data['mergeRequest']['webUrl']).to be_present + end + + it_behaves_like 'a working graphql query' + end + + context 'when the user does not have access to the merge request' do + before do + post_graphql(query, current_user: current_user) + end + + it 'returns an empty field' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['mergeRequest']).to be_nil + end + + it_behaves_like 'a working graphql query' + end +end diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb new file mode 100644 index 00000000000..8196bcfa87c --- /dev/null +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'getting project information' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { create(:user) } + + let(:query) do + graphql_query_for('project', 'fullPath' => project.full_path) + end + + context 'when the user has access to the project' do + before do + project.add_developer(current_user) + post_graphql(query, current_user: current_user) + end + + it 'includes the project' do + expect(graphql_data['project']).not_to be_nil + end + + it_behaves_like 'a working graphql query' + end + + context 'when the user does not have access to the project' do + before do + post_graphql(query, current_user: current_user) + end + + it 'returns an empty field' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).to be_nil + end + + it_behaves_like 'a working graphql query' + end +end diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb new file mode 100644 index 00000000000..5fde4bd885b --- /dev/null +++ b/spec/routing/api_routing_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'api', 'routing' do + context 'when graphql is disabled' do + before do + stub_feature_flags(graphql: false) + end + + it 'does not route to the GraphqlController' do + expect(get('/api/graphql')).not_to route_to('graphql#execute') + end + + it 'does not expose graphiql' do + expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + end + end + + context 'when graphql is disabled' do + before do + stub_feature_flags(graphql: true) + end + + it 'routes to the GraphqlController' do + expect(get('/api/graphql')).not_to route_to('graphql#execute') + end + + it 'exposes graphiql' do + expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + end + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb new file mode 100644 index 00000000000..30ff9a1196a --- /dev/null +++ b/spec/support/helpers/graphql_helpers.rb @@ -0,0 +1,90 @@ +module GraphqlHelpers + # makes an underscored string look like a fieldname + # "merge_request" => "mergeRequest" + def self.fieldnamerize(underscored_field_name) + graphql_field_name = underscored_field_name.to_s.camelize + graphql_field_name[0] = graphql_field_name[0].downcase + + graphql_field_name + end + + # Run a loader's named resolver + def resolve(resolver_class, obj: nil, args: {}, ctx: {}) + resolver_class.new(object: obj, context: ctx).resolve(args) + end + + # Runs a block inside a BatchLoader::Executor wrapper + def batch(max_queries: nil, &blk) + wrapper = proc do + begin + BatchLoader::Executor.ensure_current + yield + ensure + BatchLoader::Executor.clear_current + end + end + + if max_queries + result = nil + expect { result = wrapper.call }.not_to exceed_query_limit(max_queries) + result + else + wrapper.call + end + end + + def graphql_query_for(name, attributes = {}, fields = nil) + fields ||= all_graphql_fields_for(name.classify) + attributes = attributes_to_graphql(attributes) + <<~QUERY + { + #{name}(#{attributes}) { + #{fields} + } + } + QUERY + end + + def all_graphql_fields_for(class_name) + type = GitlabSchema.types[class_name.to_s] + return "" unless type + + type.fields.map do |name, field| + if scalar?(field) + name + else + "#{name} { #{all_graphql_fields_for(field_type(field))} }" + end + end.join("\n") + end + + def attributes_to_graphql(attributes) + attributes.map do |name, value| + "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\"" + end.join(", ") + end + + def post_graphql(query, current_user: nil) + post api('/', current_user, version: 'graphql'), query: query + end + + def graphql_data + json_response['data'] + end + + def graphql_errors + json_response['data'] + end + + def scalar?(field) + field_type(field).kind.scalar? + end + + def field_type(field) + if field.type.respond_to?(:of_type) + field.type.of_type + else + field.type + end + end +end diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb new file mode 100644 index 00000000000..ba7a1c8cde0 --- /dev/null +++ b/spec/support/matchers/graphql_matchers.rb @@ -0,0 +1,40 @@ +RSpec::Matchers.define :require_graphql_authorizations do |*expected| + match do |field| + field_definition = field.metadata[:type_class] + expect(field_definition).to respond_to(:required_permissions) + expect(field_definition.required_permissions).to contain_exactly(*expected) + end +end + +RSpec::Matchers.define :have_graphql_fields do |*expected| + match do |kls| + field_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) } + expect(kls.fields.keys).to contain_exactly(*field_names) + end +end + +RSpec::Matchers.define :have_graphql_arguments do |*expected| + include GraphqlHelpers + + match do |field| + argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) } + expect(field.arguments.keys).to contain_exactly(*argument_names) + end +end + +RSpec::Matchers.define :have_graphql_type do |expected| + match do |field| + expect(field.type).to eq(expected.to_graphql) + end +end + +RSpec::Matchers.define :have_graphql_resolver do |expected| + match do |field| + case expected + when Method + expect(field.metadata[:type_class].resolve_proc).to eq(expected) + else + expect(field.metadata[:type_class].resolver).to eq(expected) + end + end +end diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb new file mode 100644 index 00000000000..9b2b74593a5 --- /dev/null +++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +shared_examples 'a working graphql query' do + include GraphqlHelpers + + it 'is returns a successfull response', :aggregate_failures do + expect(response).to be_success + expect(graphql_errors['errors']).to be_nil + expect(json_response.keys).to include('data') + end +end |