diff options
Diffstat (limited to 'spec/graphql/resolvers')
34 files changed, 856 insertions, 62 deletions
diff --git a/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb b/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb index c5637d43382..578d679ade4 100644 --- a/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb +++ b/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb @@ -14,7 +14,9 @@ RSpec.describe Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsReso let_it_be(:project_measurement_new) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) } let_it_be(:project_measurement_old) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) } - subject { resolve_measurements({ identifier: 'projects' }, { current_user: current_user }) } + let(:arguments) { { identifier: 'projects' } } + + subject { resolve_measurements(arguments, { current_user: current_user }) } context 'when requesting project count measurements' do context 'as an admin user' do @@ -40,6 +42,24 @@ RSpec.describe Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsReso expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end end + + context 'when filtering by recorded_after and recorded_before' do + before do + arguments[:recorded_after] = 4.days.ago + arguments[:recorded_before] = 1.day.ago + end + + it { is_expected.to match_array([project_measurement_new]) } + + context 'when "incorrect" values are passed' do + before do + arguments[:recorded_after] = 1.day.ago + arguments[:recorded_before] = 4.days.ago + end + + it { is_expected.to be_empty } + end + end end context 'when requesting pipeline counts by pipeline status' do diff --git a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb new file mode 100644 index 00000000000..36e409e0677 --- /dev/null +++ b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:prometheus_integration) { create(:prometheus_service, project: project) } + let_it_be(:active_http_integration) { create(:alert_management_http_integration, project: project) } + let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) } + let_it_be(:other_proj_integration) { create(:alert_management_http_integration) } + + subject { sync(resolve_http_integrations) } + + specify do + expect(described_class).to have_nullable_graphql_type(Types::AlertManagement::IntegrationType.connection_type) + end + + context 'user does not have permission' do + it { is_expected.to be_empty } + end + + context 'user has permission' do + before do + project.add_maintainer(current_user) + end + + it { is_expected.to contain_exactly(active_http_integration, prometheus_integration) } + end + + private + + def resolve_http_integrations(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: project, ctx: context) + end +end diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb index 40dc2370052..e5b9fb57e42 100644 --- a/spec/graphql/resolvers/base_resolver_spec.rb +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -7,10 +7,13 @@ RSpec.describe Resolvers::BaseResolver do let(:resolver) do Class.new(described_class) do - def resolve(**args) + argument :test, ::GraphQL::INT_TYPE, required: false + type [::GraphQL::INT_TYPE], null: true + + def resolve(test: 100) process(object) - [args, args] + [test, test] end def process(obj); end @@ -19,17 +22,75 @@ RSpec.describe Resolvers::BaseResolver do let(:last_resolver) do Class.new(described_class) do + type [::GraphQL::INT_TYPE], null: true + def resolve(**args) [1, 2] end end end + describe '.singular_type' do + subject { resolver.singular_type } + + context 'for a connection of scalars' do + let(:resolver) do + Class.new(described_class) do + type ::GraphQL::INT_TYPE.connection_type, null: true + end + end + + it { is_expected.to eq(::GraphQL::INT_TYPE) } + end + + context 'for a connection of objects' do + let(:object) do + Class.new(::Types::BaseObject) do + graphql_name 'Foo' + end + end + + let(:resolver) do + conn = object.connection_type + + Class.new(described_class) do + type conn, null: true + end + end + + it { is_expected.to eq(object) } + end + + context 'for a list type' do + let(:resolver) do + Class.new(described_class) do + type [::GraphQL::STRING_TYPE], null: true + end + end + + it { is_expected.to eq(::GraphQL::STRING_TYPE) } + end + + context 'for a scalar type' do + let(:resolver) do + Class.new(described_class) do + type ::GraphQL::BOOLEAN_TYPE, null: true + end + end + + it { is_expected.to eq(::GraphQL::BOOLEAN_TYPE) } + end + end + describe '.single' do it 'returns a subclass from the resolver' do expect(resolver.single.superclass).to eq(resolver) end + it 'has the correct (singular) type' do + expect(resolver.single.type).to eq(::GraphQL::INT_TYPE) + end + it 'returns the same subclass every time' do expect(resolver.single.object_id).to eq(resolver.single.object_id) end @@ -37,15 +98,106 @@ RSpec.describe Resolvers::BaseResolver do it 'returns a resolver that gives the first result from the original resolver' do result = resolve(resolver.single, args: { test: 1 }) - expect(result).to eq(test: 1) + expect(result).to eq(1) + end + end + + describe '.when_single' do + let(:resolver) do + Class.new(described_class) do + type [::GraphQL::INT_TYPE], null: true + + when_single do + argument :foo, ::GraphQL::INT_TYPE, required: true + end + + def resolve(foo: 1) + [foo * foo] # rubocop: disable Lint/BinaryOperatorWithIdenticalOperands + end + end + end + + it 'does not apply the block to the resolver' do + expect(resolver.field_options).to include( + arguments: be_empty + ) + result = resolve(resolver) + + expect(result).to eq([1]) + end + + it 'applies the block to the single version of the resolver' do + expect(resolver.single.field_options).to include( + arguments: match('foo' => an_instance_of(::Types::BaseArgument)) + ) + result = resolve(resolver.single, args: { foo: 7 }) + + expect(result).to eq(49) + end + + context 'multiple when_single blocks' do + let(:resolver) do + Class.new(described_class) do + type [::GraphQL::INT_TYPE], null: true + + when_single do + argument :foo, ::GraphQL::INT_TYPE, required: true + end + + when_single do + argument :bar, ::GraphQL::INT_TYPE, required: true + end + + def resolve(foo: 1, bar: 2) + [foo * bar] + end + end + end + + it 'applies both blocks to the single version of the resolver' do + expect(resolver.single.field_options).to include( + arguments: match('foo' => ::Types::BaseArgument, 'bar' => ::Types::BaseArgument) + ) + result = resolve(resolver.single, args: { foo: 7, bar: 5 }) + + expect(result).to eq(35) + end + end + + context 'inheritance' do + let(:subclass) do + Class.new(resolver) do + when_single do + argument :inc, ::GraphQL::INT_TYPE, required: true + end + + def resolve(foo:, inc:) + super(foo: foo + inc) + end + end + end + + it 'applies both blocks to the single version of the resolver' do + expect(resolver.single.field_options).to include( + arguments: match('foo' => ::Types::BaseArgument) + ) + expect(subclass.single.field_options).to include( + arguments: match('foo' => ::Types::BaseArgument, 'inc' => ::Types::BaseArgument) + ) + result = resolve(subclass.single, args: { foo: 7, inc: 1 }) + + expect(result).to eq(64) + end end end context 'when the resolver returns early' do let(:resolver) do Class.new(described_class) do + type [::GraphQL::STRING_TYPE], null: true + def ready?(**args) - [false, %w(early return)] + [false, %w[early return]] end def resolve(**args) @@ -121,28 +273,4 @@ RSpec.describe Resolvers::BaseResolver do end end end - - describe '#synchronized_object' do - let(:object) { double(foo: :the_foo) } - - let(:resolver) do - Class.new(described_class) do - def resolve(**args) - [synchronized_object.foo] - end - end - end - - it 'handles raw objects' do - expect(resolve(resolver, obj: object)).to contain_exactly(:the_foo) - end - - it 'handles lazy objects' do - delayed = BatchLoader::GraphQL.for(1).batch do |_, loader| - loader.call(1, object) - end - - expect(resolve(resolver, obj: delayed)).to contain_exactly(:the_foo) - end - end end diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb new file mode 100644 index 00000000000..a836c89bd61 --- /dev/null +++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::JobsResolver do + include GraphqlHelpers + + let_it_be(:pipeline) { create(:ci_pipeline) } + + before_all do + create(:ci_build, name: 'Normal job', pipeline: pipeline) + create(:ci_build, :sast, name: 'DAST job', pipeline: pipeline) + create(:ci_build, :dast, name: 'SAST job', pipeline: pipeline) + create(:ci_build, :container_scanning, name: 'Container scanning job', pipeline: pipeline) + end + + describe '#resolve' do + context 'when security_report_types is empty' do + it "returns all of the pipeline's jobs" do + jobs = resolve(described_class, obj: pipeline, args: {}, ctx: {}) + + job_names = jobs.map(&:name) + expect(job_names).to contain_exactly('Normal job', 'DAST job', 'SAST job', 'Container scanning job') + end + end + + context 'when security_report_types is present' do + it "returns the pipeline's jobs with the given security report types" do + report_types = [ + ::Types::Security::ReportTypeEnum.values['SAST'].value, + ::Types::Security::ReportTypeEnum.values['DAST'].value + ] + jobs = resolve(described_class, obj: pipeline, args: { security_report_types: report_types }, ctx: {}) + + job_names = jobs.map(&:name) + expect(job_names).to contain_exactly('DAST job', 'SAST job') + end + end + end +end diff --git a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb new file mode 100644 index 00000000000..3d004290d9b --- /dev/null +++ b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::RunnerSetupResolver do + include GraphqlHelpers + + describe '#resolve' do + let(:user) { create(:user) } + + subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: platform, architecture: 'amd64' }.merge(target_param)) } + + context 'with container platforms' do + let(:platform) { 'docker' } + let(:project) { create(:project) } + let(:target_param) { { project_id: project.to_global_id } } + + it 'returns install instructions' do + expect(resolve_subject[:install_instructions]).not_to eq(nil) + end + + it 'does not return register instructions' do + expect(resolve_subject[:register_instructions]).to eq(nil) + end + end + + context 'with regular platforms' do + let(:platform) { 'linux' } + + context 'without target parameter' do + let(:target_param) { {} } + + context 'when user is not admin' do + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is admin' do + before do + user.update!(admin: true) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + end + + context 'with project target parameter' do + let(:project) { create(:project) } + let(:target_param) { { project_id: project.to_global_id } } + + context 'when user has access to admin builds on project' do + before do + project.add_maintainer(user) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + + context 'when user does not have access to admin builds on project' do + before do + project.add_developer(user) + end + + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'with group target parameter' do + let(:group) { create(:group) } + let(:target_param) { { group_id: group.to_global_id } } + + context 'when user has access to admin builds on group' do + before do + group.add_owner(user) + end + + it 'returns install and register instructions' do + expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions) + expect(resolve_subject.values).not_to include(nil) + end + end + + context 'when user does not have access to admin builds on group' do + before do + group.add_developer(user) + end + + it 'returns access error' do + expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + end + end +end diff --git a/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb new file mode 100644 index 00000000000..b6fe94a2312 --- /dev/null +++ b/spec/graphql/resolvers/concerns/caching_array_resolver_spec.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::CachingArrayResolver do + include GraphqlHelpers + + let_it_be(:non_admins) { create_list(:user, 4, admin: false) } + let(:query_context) { {} } + let(:max_page_size) { 10 } + let(:field) { double('Field', max_page_size: max_page_size) } + let(:schema) { double('Schema', default_max_page_size: 3) } + + let_it_be(:caching_resolver) do + mod = described_class + + Class.new(::Resolvers::BaseResolver) do + include mod + + def query_input(is_admin:) + is_admin + end + + def query_for(is_admin) + if is_admin.nil? + model_class.all + else + model_class.where(admin: is_admin) + end + end + + def model_class + User # Happens to include FromUnion, and is cheap-ish to create + end + end + end + + describe '#resolve' do + context 'there are more than MAX_UNION_SIZE queries' do + let_it_be(:max_union) { 3 } + let_it_be(:resolver) do + mod = described_class + max = max_union + + Class.new(::Resolvers::BaseResolver) do + include mod + + def query_input(username:) + username + end + + def query_for(username) + if username.nil? + model_class.all + else + model_class.where(username: username) + end + end + + def model_class + User # Happens to include FromUnion, and is cheap-ish to create + end + + define_method :max_union_size do + max + end + end + end + + it 'executes the queries in multiple batches' do + users = create_list(:user, (max_union * 2) + 1) + expect(User).to receive(:from_union).twice.and_call_original + + results = users.in_groups_of(2, false).map do |users| + resolve(resolver, args: { username: users.map(&:username) }, field: field, schema: schema) + end + + expect(results.flat_map(&method(:force))).to match_array(users) + end + end + + context 'all queries return results' do + let_it_be(:admins) { create_list(:admin, 3) } + + it 'batches the queries' do + expect do + [resolve_users(true), resolve_users(false)].each(&method(:force)) + end.to issue_same_number_of_queries_as { force(resolve_users(nil)) } + end + + it 'finds the correct values' do + found_admins = resolve_users(true) + found_others = resolve_users(false) + admins_again = resolve_users(true) + found_all = resolve_users(nil) + + expect(force(found_admins)).to match_array(admins) + expect(force(found_others)).to match_array(non_admins) + expect(force(admins_again)).to match_array(admins) + expect(force(found_all)).to match_array(admins + non_admins) + end + end + + it 'does not perform a union of a query with itself' do + expect(User).to receive(:where).once.and_call_original + + [resolve_users(false), resolve_users(false)].each(&method(:force)) + end + + context 'one of the queries returns no results' do + it 'finds the correct values' do + found_admins = resolve_users(true) + found_others = resolve_users(false) + found_all = resolve_users(nil) + + expect(force(found_admins)).to be_empty + expect(force(found_others)).to match_array(non_admins) + expect(force(found_all)).to match_array(non_admins) + end + end + + context 'one of the queries has already been cached' do + before do + force(resolve_users(nil)) + end + + it 'avoids further queries' do + expect do + repeated_find = resolve_users(nil) + + expect(force(repeated_find)).to match_array(non_admins) + end.not_to exceed_query_limit(0) + end + end + + context 'the resolver overrides item_found' do + let_it_be(:admins) { create_list(:admin, 2) } + let(:query_context) do + { + found: { true => [], false => [], nil => [] } + } + end + + let_it_be(:with_item_found) do + Class.new(caching_resolver) do + def item_found(key, item) + context[:found][key] << item + end + end + end + + it 'receives item_found for each key the item mapped to' do + found_admins = resolve_users(true, with_item_found) + found_all = resolve_users(nil, with_item_found) + + [found_admins, found_all].each(&method(:force)) + + expect(query_context[:found]).to match({ + false => be_empty, + true => match_array(admins), + nil => match_array(admins + non_admins) + }) + end + end + + context 'the max_page_size is lower than the total result size' do + let(:max_page_size) { 2 } + + it 'respects the max_page_size, on a per subset basis' do + found_all = resolve_users(nil) + found_others = resolve_users(false) + + expect(force(found_all).size).to eq(2) + expect(force(found_others).size).to eq(2) + end + end + + context 'the field does not declare max_page_size' do + let(:max_page_size) { nil } + + it 'takes the page size from schema.default_max_page_size' do + found_all = resolve_users(nil) + found_others = resolve_users(false) + + expect(force(found_all).size).to eq(schema.default_max_page_size) + expect(force(found_others).size).to eq(schema.default_max_page_size) + end + end + + specify 'force . resolve === to_a . query_for . query_input' do + r = resolver_instance(caching_resolver) + args = { is_admin: false } + + naive = r.query_for(r.query_input(**args)).to_a + + expect(force(r.resolve(**args))).to eq(naive) + end + end + + def resolve_users(is_admin, resolver = caching_resolver) + args = { is_admin: is_admin } + resolve(resolver, args: args, field: field, ctx: query_context, schema: schema) + end + + def force(lazy) + ::Gitlab::Graphql::Lazy.force(lazy) + end +end diff --git a/spec/graphql/resolvers/container_repositories_resolver_spec.rb b/spec/graphql/resolvers/container_repositories_resolver_spec.rb new file mode 100644 index 00000000000..b888d79626e --- /dev/null +++ b/spec/graphql/resolvers/container_repositories_resolver_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::ContainerRepositoriesResolver do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } + let_it_be(:container_repositories) { create(:container_repository, project: project) } + + let(:args) { {} } + + describe '#resolve' do + let(:object) { project } + + subject { resolve(described_class, ctx: { current_user: user }, args: args, obj: object) } + + shared_examples 'returning container repositories' do + it { is_expected.to contain_exactly(container_repositories) } + + context 'with a named search' do + let_it_be(:named_container_repository) { create(:container_repository, project: project, name: 'Foobar') } + + let(:args) { { name: 'ooba' } } + + it { is_expected.to contain_exactly(named_container_repository) } + end + end + + context 'with authorized user' do + before do + group.add_user(user, :maintainer) + end + + context 'when the object is a project' do + it_behaves_like 'returning container repositories' + end + + context 'when the object is a group' do + let(:object) { group } + + it_behaves_like 'returning container repositories' + end + + context 'when the object is an invalid type' do + let(:object) { Object.new } + + it { expect { subject }.to raise_exception('invalid subject_type') } + end + end + + context 'with unauthorized user' do + it { is_expected.to be nil } + end + end +end diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb index 02d7f94612c..e33eaedf167 100644 --- a/spec/graphql/resolvers/design_management/design_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb @@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do include GraphqlHelpers include DesignManagementTestHelpers + specify do + expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType) + end + before do enable_design_management end @@ -57,12 +61,21 @@ RSpec.describe Resolvers::DesignManagement::DesignResolver do end context 'the ID belongs to a design on another issue' do - let(:args) { { id: GitlabSchema.id_from_object(design_on_other_issue).to_s } } + let(:args) { { id: global_id_of(design_on_other_issue) } } it 'returns nothing' do expect(resolve_design).to be_nil end end + + context 'the ID does not belong to a design at all' do + let(:args) { { id: global_id_of(issue) } } + let(:msg) { /does not represent an instance of DesignManagement::Design/ } + + it 'complains meaningfully' do + expect { resolve_design }.to raise_error(msg) + end + end end context 'by filename' do diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb index cfa37d34fd9..28e963c88a9 100644 --- a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb @@ -6,6 +6,10 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do include GraphqlHelpers include DesignManagementTestHelpers + specify do + expect(described_class).to have_nullable_graphql_type(::Types::DesignManagement::DesignType.connection_type) + end + before do enable_design_management end @@ -65,8 +69,24 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do let(:second_version) { create(:design_version) } let(:second_design) { create(:design, issue: issue, versions: [second_version]) } + context 'ids is provided but null' do + let(:args) { { ids: nil } } + + it 'behaves as if unfiltered' do + expect(resolve_designs).to contain_exactly(first_design, second_design) + end + end + + context 'ids is provided but empty' do + let(:args) { { ids: [] } } + + it 'eliminates all values' do + expect(resolve_designs).to be_empty + end + end + context 'the ID is on the current issue' do - let(:args) { { ids: [GitlabSchema.id_from_object(second_design).to_s] } } + let(:args) { { ids: [GitlabSchema.id_from_object(second_design)] } } it 'resolves to just the relevant design' do expect(resolve_designs).to contain_exactly(second_design) @@ -77,7 +97,7 @@ RSpec.describe Resolvers::DesignManagement::DesignsResolver do let(:third_version) { create(:design_version) } let(:third_design) { create(:design, issue: create(:issue, project: project), versions: [third_version]) } - let(:args) { { ids: [GitlabSchema.id_from_object(third_design).to_s] } } + let(:args) { { ids: [GitlabSchema.id_from_object(third_design)] } } it 'ignores it' do expect(resolve_designs).to be_empty diff --git a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb index 8ad928e9854..403261fc22a 100644 --- a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Resolvers::DesignManagement::VersionInCollectionResolver do end context 'we pass an id' do - let(:params) { { id: global_id_of(first_version) } } + let(:params) { { version_id: global_id_of(first_version) } } it { is_expected.to eq(first_version) } end @@ -44,13 +44,14 @@ RSpec.describe Resolvers::DesignManagement::VersionInCollectionResolver do end context 'we pass an inconsistent mixture of sha and version id' do - let(:params) { { sha: first_version.sha, id: global_id_of(create(:design_version)) } } + let(:params) { { sha: first_version.sha, version_id: global_id_of(create(:design_version)) } } it { is_expected.to be_nil } end context 'we pass the id of something that is not a design_version' do - let(:params) { { id: global_id_of(project) } } + let(:params) { { version_id: global_id_of(project) } } + let(:appropriate_error) { ::GraphQL::CoercionError } it 'raises an appropriate error' do expect { result }.to raise_error(appropriate_error) diff --git a/spec/graphql/resolvers/echo_resolver_spec.rb b/spec/graphql/resolvers/echo_resolver_spec.rb index 2182ac221f6..4f48e5e0d7a 100644 --- a/spec/graphql/resolvers/echo_resolver_spec.rb +++ b/spec/graphql/resolvers/echo_resolver_spec.rb @@ -8,6 +8,10 @@ RSpec.describe Resolvers::EchoResolver do let(:current_user) { create(:user) } let(:text) { 'Message test' } + specify do + expect(described_class).to have_non_null_graphql_type(::GraphQL::STRING_TYPE) + end + describe '#resolve' do it 'echoes text and username' do expect(resolve_echo(text)).to eq %Q("#{current_user.username}" says: #{text}) diff --git a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb index 7e531910184..bf8d2139c82 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_detailed_error_resolver_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do let(:issue_details_service) { spy('ErrorTracking::IssueDetailsService') } + specify do + expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryDetailedErrorType) + end + before do project.add_developer(current_user) @@ -61,7 +65,9 @@ RSpec.describe Resolvers::ErrorTracking::SentryDetailedErrorResolver do context 'blank id' do let(:args) { { id: '' } } - it_behaves_like 'it resolves to nil' + it 'responds with an error' do + expect { resolve_error(args) }.to raise_error(::GraphQL::CoercionError) + end end end diff --git a/spec/graphql/resolvers/error_tracking/sentry_error_collection_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_error_collection_resolver_spec.rb index 02e0420be2a..20c2bdcd4e1 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_error_collection_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_error_collection_resolver_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') } + specify do + expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryErrorCollectionType) + end + before do project.add_developer(current_user) diff --git a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb index 554873a6e21..edca11f40d7 100644 --- a/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb +++ b/spec/graphql/resolvers/error_tracking/sentry_errors_resolver_spec.rb @@ -14,6 +14,10 @@ RSpec.describe Resolvers::ErrorTracking::SentryErrorsResolver do let(:issues) { nil } let(:pagination) { nil } + specify do + expect(described_class).to have_nullable_graphql_type(Types::ErrorTracking::SentryErrorType.connection_type) + end + describe '#resolve' do context 'insufficient user permission' do let(:user) { create(:user) } diff --git a/spec/graphql/resolvers/group_members_resolver_spec.rb b/spec/graphql/resolvers/group_members_resolver_spec.rb index bbfea575492..bd0b4870062 100644 --- a/spec/graphql/resolvers/group_members_resolver_spec.rb +++ b/spec/graphql/resolvers/group_members_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::GroupMembersResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::GroupMemberType.connection_type) + end + it_behaves_like 'querying members with a group' do let_it_be(:resource_member) { create(:group_member, user: user_1, group: group_1) } let_it_be(:resource) { group_1 } diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 3a6507f906c..43cbd4d2bdd 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -20,6 +20,10 @@ RSpec.describe Resolvers::IssuesResolver do let_it_be(:label1) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) } + specify do + expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type) + end + context "with a project" do before do project.add_developer(current_user) diff --git a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb index ae3097c1d9e..deb5ff584cf 100644 --- a/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_request_pipelines_resolver_spec.rb @@ -14,6 +14,7 @@ RSpec.describe Resolvers::MergeRequestPipelinesResolver do sha: merge_request.diff_head_sha ) end + let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: merge_request.source_project, ref: 'other-ref') } let_it_be(:other_pipeline) { create(:ci_pipeline) } let(:current_user) { create(:user) } diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index aecffc487aa..3a3393a185c 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -34,13 +34,13 @@ RSpec.describe Resolvers::MergeRequestsResolver do context 'no arguments' do it 'returns all merge requests' do - result = resolve_mr(project, {}) + result = resolve_mr(project) expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone) end it 'returns only merge requests that the current user can see' do - result = resolve_mr(project, {}, user: build(:user)) + result = resolve_mr(project, user: build(:user)) expect(result).to be_empty end @@ -236,10 +236,10 @@ RSpec.describe Resolvers::MergeRequestsResolver do end def resolve_mr_single(project, iid) - resolve_mr(project, { iids: iid }, resolver: described_class.single) + resolve_mr(project, resolver: described_class.single, iids: iid) end - def resolve_mr(project, args, resolver: described_class, user: current_user) + def resolve_mr(project, resolver: described_class, user: current_user, **args) resolve(resolver, obj: project, args: args, ctx: { current_user: user }) end end diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb index 20556941de4..f8c01f9d531 100644 --- a/spec/graphql/resolvers/metadata_resolver_spec.rb +++ b/spec/graphql/resolvers/metadata_resolver_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Resolvers::MetadataResolver do describe '#resolve' do it 'returns version and revision' do - expect(resolve(described_class)).to eq(version: Gitlab::VERSION, revision: Gitlab.revision) + expect(resolve(described_class)).to have_attributes(version: Gitlab::VERSION, revision: Gitlab.revision) end end end diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb index a6a86c49373..1950c2ca067 100644 --- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb @@ -10,6 +10,10 @@ RSpec.describe Resolvers::ProjectPipelineResolver do let_it_be(:other_pipeline) { create(:ci_pipeline) } let(:current_user) { create(:user) } + specify do + expect(described_class).to have_nullable_graphql_type(::Types::Ci::PipelineType) + end + def resolve_pipeline(project, args) resolve(described_class, obj: project, args: args, ctx: { current_user: current_user }) end diff --git a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb index 0775c1c31d1..ad59cb6b95e 100644 --- a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::Projects::JiraImportsResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::JiraImportType.connection_type) + end + describe '#resolve' do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) } diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb index 840aea8b8c4..c375345250d 100644 --- a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::Projects::JiraProjectsResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::Projects::Services::JiraProjectType.connection_type) + end + describe '#resolve' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } diff --git a/spec/graphql/resolvers/projects/services_resolver_spec.rb b/spec/graphql/resolvers/projects/services_resolver_spec.rb index 8b6eff9e8b6..a1b631113b2 100644 --- a/spec/graphql/resolvers/projects/services_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/services_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::Projects::ServicesResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::Projects::ServiceType.connection_type) + end + describe '#resolve' do let_it_be(:user) { create(:user) } diff --git a/spec/graphql/resolvers/projects/snippets_resolver_spec.rb b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb index b4a5eb8ddb0..6f7feff8fe5 100644 --- a/spec/graphql/resolvers/projects/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb @@ -56,12 +56,6 @@ RSpec.describe Resolvers::Projects::SnippetsResolver do expect(snippets).to contain_exactly(project_snippet, other_project_snippet) end - - it 'returns an error if the gid is invalid' do - expect do - resolve_snippets(args: { ids: 'foo' }) - end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) - end end context 'when no project is provided' do diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb index 83a26062957..3de54c7e410 100644 --- a/spec/graphql/resolvers/projects_resolver_spec.rb +++ b/spec/graphql/resolvers/projects_resolver_spec.rb @@ -134,8 +134,8 @@ RSpec.describe Resolvers::ProjectsResolver do is_expected.to eq([named_project3, named_project1, named_project2]) end - it 'returns projects not in order of similarity to search if flag is off' do - is_expected.not_to eq([named_project3, named_project1, named_project2]) + it 'returns projects in any order if flag is off' do + is_expected.to match_array([named_project3, named_project1, named_project2]) end end end diff --git a/spec/graphql/resolvers/release_resolver_spec.rb b/spec/graphql/resolvers/release_resolver_spec.rb index 666d54fbc3c..04765fc68e9 100644 --- a/spec/graphql/resolvers/release_resolver_spec.rb +++ b/spec/graphql/resolvers/release_resolver_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Resolvers::ReleaseResolver do let(:args) { {} } it 'raises an error' do - expect { resolve_release }.to raise_error(ArgumentError, "missing keyword: tag_name") + expect { resolve_release }.to raise_error(ArgumentError, "missing keyword: :tag_name") end end end diff --git a/spec/graphql/resolvers/releases_resolver_spec.rb b/spec/graphql/resolvers/releases_resolver_spec.rb index ee8b33fc748..b9b90686aa7 100644 --- a/spec/graphql/resolvers/releases_resolver_spec.rb +++ b/spec/graphql/resolvers/releases_resolver_spec.rb @@ -5,12 +5,19 @@ require 'spec_helper' RSpec.describe Resolvers::ReleasesResolver do include GraphqlHelpers + let_it_be(:today) { Time.now } + let_it_be(:yesterday) { today - 1.day } + let_it_be(:tomorrow) { today + 1.day } + let_it_be(:project) { create(:project, :private) } - let_it_be(:release_v1) { create(:release, project: project, tag: 'v1.0.0') } - let_it_be(:release_v2) { create(:release, project: project, tag: 'v2.0.0') } + let_it_be(:release_v1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) } + let_it_be(:release_v2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) } + let_it_be(:release_v3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) } let_it_be(:developer) { create(:user) } let_it_be(:public_user) { create(:user) } + let(:args) { { sort: :released_at_desc } } + before do project.add_developer(developer) end @@ -28,7 +35,41 @@ RSpec.describe Resolvers::ReleasesResolver do let(:current_user) { developer } it 'returns all releases associated to the project' do - expect(resolve_releases).to eq([release_v1, release_v2]) + expect(resolve_releases).to eq([release_v3, release_v2, release_v1]) + end + + describe 'sorting behavior' do + context 'with sort: :released_at_desc' do + let(:args) { { sort: :released_at_desc } } + + it 'returns the releases ordered by released_at in descending order' do + expect(resolve_releases).to eq([release_v3, release_v2, release_v1]) + end + end + + context 'with sort: :released_at_asc' do + let(:args) { { sort: :released_at_asc } } + + it 'returns the releases ordered by released_at in ascending order' do + expect(resolve_releases).to eq([release_v1, release_v2, release_v3]) + end + end + + context 'with sort: :created_desc' do + let(:args) { { sort: :created_desc } } + + it 'returns the releases ordered by created_at in descending order' do + expect(resolve_releases).to eq([release_v1, release_v3, release_v2]) + end + end + + context 'with sort: :created_asc' do + let(:args) { { sort: :created_asc } } + + it 'returns the releases ordered by created_at in ascending order' do + expect(resolve_releases).to eq([release_v2, release_v3, release_v1]) + end + end end end end @@ -37,6 +78,6 @@ RSpec.describe Resolvers::ReleasesResolver do def resolve_releases context = { current_user: current_user } - resolve(described_class, obj: project, args: {}, ctx: context) + resolve(described_class, obj: project, args: args, ctx: context) end end diff --git a/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb b/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb index fdbd87c32be..16e69f662c0 100644 --- a/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb +++ b/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::Snippets::BlobsResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::Snippets::BlobType.connection_type) + end + describe '#resolve' do let_it_be(:current_user) { create(:user) } let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) } diff --git a/spec/graphql/resolvers/snippets_resolver_spec.rb b/spec/graphql/resolvers/snippets_resolver_spec.rb index 180be8e8624..a58d9c5ac3a 100644 --- a/spec/graphql/resolvers/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/snippets_resolver_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Resolvers::SnippetsResolver do context 'when using filters' do context 'by author id' do it 'returns the snippets' do - snippets = resolve_snippets(args: { author_id: current_user.to_global_id }) + snippets = resolve_snippets(args: { author_id: global_id_of(current_user) }) expect(snippets).to contain_exactly(personal_snippet, project_snippet) end @@ -44,7 +44,7 @@ RSpec.describe Resolvers::SnippetsResolver do it 'returns an error if the param id is invalid' do expect do resolve_snippets(args: { author_id: 'foo' }) - end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end.to raise_error(GraphQL::CoercionError) end end @@ -65,7 +65,7 @@ RSpec.describe Resolvers::SnippetsResolver do it 'returns an error if the param id is invalid' do expect do resolve_snippets(args: { project_id: 'foo' }) - end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end.to raise_error(GraphQL::CoercionError) end end @@ -99,14 +99,14 @@ RSpec.describe Resolvers::SnippetsResolver do expect(snippets).to contain_exactly(personal_snippet, project_snippet) end - it 'returns an error if the gid is invalid' do + it 'returns an error if the id cannot be coerced' do args = { ids: [personal_snippet.to_global_id, 'foo'] } expect do resolve_snippets(args: args) - end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end.to raise_error(GraphQL::CoercionError, '"foo" is not a valid Global ID') end it 'returns an error if both project and author are provided' do diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todo_resolver_spec.rb index 83e3140b676..c764f389c16 100644 --- a/spec/graphql/resolvers/todo_resolver_spec.rb +++ b/spec/graphql/resolvers/todo_resolver_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' RSpec.describe Resolvers::TodoResolver do include GraphqlHelpers + specify do + expect(described_class).to have_nullable_graphql_type(Types::TodoType.connection_type) + end + describe '#resolve' do let_it_be(:current_user) { create(:user) } let_it_be(:author1) { create(:user) } diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb index 7818c25fe47..9eafd272771 100644 --- a/spec/graphql/resolvers/tree_resolver_spec.rb +++ b/spec/graphql/resolvers/tree_resolver_spec.rb @@ -7,6 +7,10 @@ RSpec.describe Resolvers::TreeResolver do let(:repository) { create(:project, :repository).repository } + specify do + expect(described_class).to have_nullable_graphql_type(Types::Tree::TreeType) + end + describe '#resolve' do it 'resolves to a tree' do result = resolve_repository({ ref: "master" }) diff --git a/spec/graphql/resolvers/users/group_count_resolver_spec.rb b/spec/graphql/resolvers/users/group_count_resolver_spec.rb new file mode 100644 index 00000000000..47160a33646 --- /dev/null +++ b/spec/graphql/resolvers/users/group_count_resolver_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Users::GroupCountResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:project) { create(:project, group: create(:group)) } + let_it_be(:group_member1) { create(:group_member, source: group1, user_id: user1.id, access_level: Gitlab::Access::OWNER) } + let_it_be(:project_member1) { create(:project_member, source: project, user_id: user1.id, access_level: Gitlab::Access::DEVELOPER) } + let_it_be(:group_member2) { create(:group_member, source: group2, user_id: user2.id, access_level: Gitlab::Access::DEVELOPER) } + + it 'resolves group count for users' do + current_user = user1 + + result = batch_sync do + [user1, user2].map { |user| resolve_group_count(user, current_user) } + end + + expect(result).to eq([2, nil]) + end + + context 'permissions' do + context 'when current_user is an admin', :enable_admin_mode do + let_it_be(:admin) { create(:admin) } + + it do + result = batch_sync do + [user1, user2].map { |user| resolve_group_count(user, admin) } + end + + expect(result).to eq([2, 1]) + end + end + + context 'when current_user does not have access to the requested resource' do + it do + result = batch_sync { resolve_group_count(user1, user2) } + + expect(result).to be nil + end + end + + context 'when current_user does not exist' do + it do + result = batch_sync { resolve_group_count(user1, nil) } + + expect(result).to be nil + end + end + end + end + + def resolve_group_count(user, current_user) + resolve(described_class, obj: user, ctx: { current_user: current_user }) + end +end diff --git a/spec/graphql/resolvers/users/snippets_resolver_spec.rb b/spec/graphql/resolvers/users/snippets_resolver_spec.rb index 497b6b11b46..9ccbebc59e6 100644 --- a/spec/graphql/resolvers/users/snippets_resolver_spec.rb +++ b/spec/graphql/resolvers/users/snippets_resolver_spec.rb @@ -73,7 +73,7 @@ RSpec.describe Resolvers::Users::SnippetsResolver do expect do resolve_snippets(args: args) - end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) + end.to raise_error(GraphQL::CoercionError) end end end diff --git a/spec/graphql/resolvers/users_resolver_spec.rb b/spec/graphql/resolvers/users_resolver_spec.rb index e3d595e0790..1aa24055a89 100644 --- a/spec/graphql/resolvers/users_resolver_spec.rb +++ b/spec/graphql/resolvers/users_resolver_spec.rb @@ -5,8 +5,12 @@ require 'spec_helper' RSpec.describe Resolvers::UsersResolver do include GraphqlHelpers - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } + let_it_be(:user1) { create(:user, name: "SomePerson") } + let_it_be(:user2) { create(:user, username: "someone123784") } + + specify do + expect(described_class).to have_nullable_graphql_type(Types::UserType.connection_type) + end describe '#resolve' do it 'raises an error when read_users_list is not authorized' do @@ -43,6 +47,14 @@ RSpec.describe Resolvers::UsersResolver do ).to contain_exactly(user1, user2) end end + + context 'when a search term is passed' do + it 'returns all users who match', :aggregate_failures do + expect(resolve_users(search: "some")).to contain_exactly(user1, user2) + expect(resolve_users(search: "123784")).to contain_exactly(user2) + expect(resolve_users(search: "someperson")).to contain_exactly(user1) + end + end end def resolve_users(args = {}) |