summaryrefslogtreecommitdiff
path: root/spec/graphql
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 23:50:22 +0000
commit9dc93a4519d9d5d7be48ff274127136236a3adb3 (patch)
tree70467ae3692a0e35e5ea56bcb803eb512a10bedb /spec/graphql
parent4b0f34b6d759d6299322b3a54453e930c6121ff0 (diff)
downloadgitlab-ce-9dc93a4519d9d5d7be48ff274127136236a3adb3.tar.gz
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc43
Diffstat (limited to 'spec/graphql')
-rw-r--r--spec/graphql/features/authorization_spec.rb134
-rw-r--r--spec/graphql/gitlab_schema_spec.rb39
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb66
-rw-r--r--spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb46
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb14
-rw-r--r--spec/graphql/mutations/issues/set_assignees_spec.rb7
-rw-r--r--spec/graphql/mutations/merge_requests/set_assignees_spec.rb7
-rw-r--r--spec/graphql/mutations/release_asset_links/delete_spec.rb58
-rw-r--r--spec/graphql/mutations/release_asset_links/update_spec.rb2
-rw-r--r--spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb20
-rw-r--r--spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb39
-rw-r--r--spec/graphql/resolvers/blobs_resolver_spec.rb74
-rw-r--r--spec/graphql/resolvers/board_list_issues_resolver_spec.rb18
-rw-r--r--spec/graphql/resolvers/ci/jobs_resolver_spec.rb17
-rw-r--r--spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb77
-rw-r--r--spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb46
-rw-r--r--spec/graphql/resolvers/ci/test_suite_resolver_spec.rb54
-rw-r--r--spec/graphql/resolvers/concerns/looks_ahead_spec.rb9
-rw-r--r--spec/graphql/resolvers/group_milestones_resolver_spec.rb51
-rw-r--r--spec/graphql/resolvers/issue_status_counts_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb45
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb64
-rw-r--r--spec/graphql/resolvers/namespace_projects_resolver_spec.rb2
-rw-r--r--spec/graphql/resolvers/project_jobs_resolver_spec.rb51
-rw-r--r--spec/graphql/resolvers/project_pipeline_resolver_spec.rb15
-rw-r--r--spec/graphql/resolvers/repository_branch_names_resolver_spec.rb36
-rw-r--r--spec/graphql/resolvers/timelog_resolver_spec.rb168
-rw-r--r--spec/graphql/resolvers/users/snippets_resolver_spec.rb14
-rw-r--r--spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb3
-rw-r--r--spec/graphql/types/alert_management/prometheus_integration_type_spec.rb24
-rw-r--r--spec/graphql/types/base_enum_spec.rb48
-rw-r--r--spec/graphql/types/base_object_spec.rb432
-rw-r--r--spec/graphql/types/board_type_spec.rb14
-rw-r--r--spec/graphql/types/boards/board_issue_input_type_spec.rb4
-rw-r--r--spec/graphql/types/ci/job_status_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/job_type_spec.rb28
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb5
-rw-r--r--spec/graphql/types/ci/recent_failures_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/stage_type_spec.rb1
-rw-r--r--spec/graphql/types/ci/test_case_status_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/test_case_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_report_summary_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_report_total_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_suite_summary_type_spec.rb15
-rw-r--r--spec/graphql/types/ci/test_suite_type_spec.rb15
-rw-r--r--spec/graphql/types/global_id_type_spec.rb1
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/graphql/types/merge_request_review_state_enum_spec.rb18
-rw-r--r--spec/graphql/types/merge_requests/reviewer_type_spec.rb50
-rw-r--r--spec/graphql/types/milestone_type_spec.rb2
-rw-r--r--spec/graphql/types/packages/conan/file_metadatum_type_spec.rb13
-rw-r--r--spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb13
-rw-r--r--spec/graphql/types/packages/conan/metadatum_type_spec.rb13
-rw-r--r--spec/graphql/types/packages/package_details_type_spec.rb (renamed from spec/graphql/types/packages/package_without_versions_type_spec.rb)4
-rw-r--r--spec/graphql/types/packages/package_file_type_spec.rb13
-rw-r--r--spec/graphql/types/packages/package_type_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb32
-rw-r--r--spec/graphql/types/query_type_spec.rb2
-rw-r--r--spec/graphql/types/repository/blob_type_spec.rb9
-rw-r--r--spec/graphql/types/repository_type_spec.rb4
-rw-r--r--spec/graphql/types/snippet_type_spec.rb2
-rw-r--r--spec/graphql/types/timelog_type_spec.rb35
-rw-r--r--spec/graphql/types/user_merge_request_interaction_type_spec.rb116
64 files changed, 1933 insertions, 256 deletions
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
index 33b11e1ca09..64e423e2bf8 100644
--- a/spec/graphql/features/authorization_spec.rb
+++ b/spec/graphql/features/authorization_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Gitlab::Graphql::Authorize' do
+RSpec.describe 'DeclarativePolicy authorization in GraphQL ' do
include GraphqlHelpers
include Graphql::ResolverFactories
@@ -10,10 +10,14 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:permission_single) { :foo }
let(:permission_collection) { [:foo, :bar] }
let(:test_object) { double(name: 'My name') }
+ let(:authorizing_object) { test_object }
+ # to override when combining permissions
+ let(:permission_object_one) { authorizing_object }
+ let(:permission_object_two) { authorizing_object }
+
let(:query_string) { '{ item { name } }' }
let(:result) do
schema = empty_schema
- schema.use(Gitlab::Graphql::Authorize)
execute_query(query_type, schema: schema)
end
@@ -33,18 +37,25 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
shared_examples 'authorization with a collection of permissions' do
it 'returns the protected field when user has all permissions' do
- permit(*permission_collection)
+ permit_on(permission_object_one, permission_collection.first)
+ permit_on(permission_object_two, permission_collection.second)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user only has one of the permissions' do
- permit(permission_collection.first)
+ permit_on(permission_object_one, permission_collection.first)
expect(subject).to be_nil
end
- it 'returns nil when user only has none of the permissions' do
+ it 'returns nil when user only has the other of the permissions' do
+ permit_on(permission_object_two, permission_collection.second)
+
+ expect(subject).to be_nil
+ end
+
+ it 'returns nil when user has neither of the required permissions' do
expect(subject).to be_nil
end
end
@@ -56,6 +67,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
describe 'Field authorizations' do
let(:type) { type_factory }
+ let(:authorizing_object) { nil }
describe 'with a single permission' do
let(:query_type) do
@@ -71,9 +83,10 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:query_type) do
permissions = permission_collection
query_factory do |qt|
- qt.field :item, type, null: true, resolver: new_resolver(test_object) do
- authorize permissions
- end
+ qt.field :item, type,
+ null: true,
+ resolver: new_resolver(test_object),
+ authorize: permissions
end
end
@@ -110,9 +123,9 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:type) do
permissions = permission_collection
type_factory do |type|
- type.field :name, GraphQL::STRING_TYPE, null: true do
- authorize permissions
- end
+ type.field :name, GraphQL::STRING_TYPE,
+ null: true,
+ authorize: permissions
end
end
@@ -163,6 +176,7 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
end
describe 'type and field authorizations together' do
+ let(:authorizing_object) { anything }
let(:permission_1) { permission_collection.first }
let(:permission_2) { permission_collection.last }
@@ -181,7 +195,63 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
include_examples 'authorization with a collection of permissions'
end
- describe 'type authorizations when applied to a relay connection' do
+ describe 'resolver and field authorizations together' do
+ let(:permission_1) { permission_collection.first }
+ let(:permission_2) { permission_collection.last }
+ let(:type) { type_factory }
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :item, type,
+ null: true,
+ resolver: resolver,
+ authorize: permission_2
+ end
+ end
+
+ context 'when the resolver authorizes the object' do
+ let(:permission_object_one) { be_nil }
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = simple_resolver(test_object)
+ resolver.include(::Gitlab::Graphql::Authorize::AuthorizeResource)
+ resolver.authorize permission_1
+ resolver.authorizes_object!
+ resolver
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+
+ context 'when the resolver does not authorize the object, but instead calls authorized_find!' do
+ let(:permission_object_one) { test_object }
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = new_resolver(test_object, method: :find_object)
+ resolver.authorize permission_1
+ resolver
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+
+ context 'when the resolver calls authorized_find!, but does not list any permissions' do
+ let(:permission_object_two) { be_nil }
+ let(:resolver) do
+ resolver = new_resolver(test_object, method: :find_object)
+ resolver
+ end
+
+ it 'raises a configuration error' do
+ permit_on(permission_object_two, permission_collection.second)
+
+ expect { execute_query(query_type) }
+ .to raise_error(::Gitlab::Graphql::Authorize::AuthorizeResource::ConfigurationError)
+ end
+ end
+ end
+
+ describe 'when type authorizations when applied to a relay connection' do
let(:query_string) { '{ item { edges { node { name } } } }' }
let(:second_test_object) { double(name: 'Second thing') }
@@ -220,8 +290,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
let(:query_string) { '{ item(first: 1) { edges { node { name } } } }' }
it 'only checks permissions for the first object' do
- expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true }
- expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object)
+ expect(Ability)
+ .to receive(:allowed?)
+ .with(user, permission_single, test_object)
+ .and_return(true)
+ expect(Ability)
+ .not_to receive(:allowed?).with(user, permission_single, second_test_object)
expect(subject.size).to eq(1)
end
@@ -262,10 +336,12 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
end
let(:project_type) do |type|
+ issues = Issue.where(project: [visible_project, other_project]).order(id: :asc)
type_factory do |type|
type.graphql_name 'FakeProjectType'
- type.field :test_issues, issue_type.connection_type, null: false,
- resolver: new_resolver(Issue.where(project: [visible_project, other_project]).order(id: :asc))
+ type.field :test_issues, issue_type.connection_type,
+ null: false,
+ resolver: new_resolver(issues)
end
end
@@ -300,11 +376,35 @@ RSpec.describe 'Gitlab::Graphql::Authorize' do
end
end
+ describe 'Authorization on GraphQL::Execution::Execute::SKIP' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :item, [type], null: true, resolver: new_resolver(GraphQL::Execution::Execute::SKIP)
+ end
+ end
+
+ it 'skips redaction' do
+ expect(Ability).not_to receive(:allowed?)
+
+ result
+ end
+ end
+
private
def permit(*permissions)
+ permit_on(authorizing_object, *permissions)
+ end
+
+ def permit_on(object, *permissions)
permissions.each do |permission|
- allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, permission, object).and_return(true)
end
end
end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index cb2bb25b098..1f2c518f83c 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -14,10 +14,6 @@ RSpec.describe GitlabSchema do
expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::GenericTracing))
end
- it 'enables the authorization instrumenter' do
- expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation))
- end
-
it 'has the base mutation' do
expect(described_class.mutation).to eq(::Types::MutationType)
end
@@ -210,18 +206,22 @@ RSpec.describe GitlabSchema do
describe '.parse_gid' do
let_it_be(:global_id) { 'gid://gitlab/TestOne/2147483647' }
+ subject(:parse_gid) { described_class.parse_gid(global_id) }
+
before do
test_base = Class.new
test_one = Class.new(test_base)
test_two = Class.new(test_base)
+ test_three = Class.new(test_base)
stub_const('TestBase', test_base)
stub_const('TestOne', test_one)
stub_const('TestTwo', test_two)
+ stub_const('TestThree', test_three)
end
it 'parses the gid' do
- gid = described_class.parse_gid(global_id)
+ gid = parse_gid
expect(gid.model_id).to eq '2147483647'
expect(gid.model_class).to eq TestOne
@@ -231,7 +231,7 @@ RSpec.describe GitlabSchema do
let_it_be(:global_id) { 'malformed://gitlab/TestOne/2147483647' }
it 'raises an error' do
- expect { described_class.parse_gid(global_id) }
+ expect { parse_gid }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid GitLab ID.")
end
end
@@ -253,6 +253,33 @@ RSpec.describe GitlabSchema do
expect { described_class.parse_gid(global_id, expected_type: TestTwo) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestTwo.")
end
+
+ context 'when expected_type is an array' do
+ subject(:parse_gid) { described_class.parse_gid(global_id, expected_type: [TestOne, TestTwo]) }
+
+ context 'when global_id is of type TestOne' do
+ it 'returns an object of an expected type' do
+ expect(parse_gid.model_class).to eq TestOne
+ end
+ end
+
+ context 'when global_id is of type TestTwo' do
+ let_it_be(:global_id) { 'gid://gitlab/TestTwo/2147483647' }
+
+ it 'returns an object of an expected type' do
+ expect(parse_gid.model_class).to eq TestTwo
+ end
+ end
+
+ context 'when global_id is of type TestThree' do
+ let_it_be(:global_id) { 'gid://gitlab/TestThree/2147483647' }
+
+ it 'rejects an unknown type' do
+ expect { parse_gid }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "#{global_id} is not a valid ID for TestOne, TestTwo.")
+ end
+ end
+ end
end
end
diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
index 24104a20465..dd9305d2197 100644
--- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::Boards::Issues::IssueMoveList do
+ include GraphqlHelpers
+
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:board) { create(:board, group: group) }
@@ -16,9 +18,8 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
let_it_be(:existing_issue1) { create(:labeled_issue, project: project, labels: [testing], relative_position: 10) }
let_it_be(:existing_issue2) { create(:labeled_issue, project: project, labels: [testing], relative_position: 50) }
- let(:current_user) { user }
- let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
- let(:params) { { board: board, project_path: project.full_path, iid: issue1.iid } }
+ let(:current_ctx) { { current_user: user } }
+ let(:params) { { board_id: global_id_of(board), project_path: project.full_path, iid: issue1.iid } }
let(:move_params) do
{
from_list_id: list1.id,
@@ -33,26 +34,45 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
group.add_guest(guest)
end
- subject do
- mutation.resolve(**params.merge(move_params))
- end
+ describe '#resolve' do
+ subject do
+ sync(resolve(described_class, args: params.merge(move_params), ctx: current_ctx))
+ end
+
+ %i[from_list_id to_list_id].each do |arg_name|
+ context "when we only pass #{arg_name}" do
+ let(:move_params) { { arg_name => list1.id } }
- describe '#ready?' do
- it 'raises an error if required arguments are missing' do
- expect { mutation.ready?(**params) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError, "At least one of the arguments " \
- "fromListId, toListId, afterId or beforeId is required")
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ 'Both fromListId and toListId must be present'
+ )
+ end
+ end
end
- it 'raises an error if only one of fromListId and toListId is present' do
- expect { mutation.ready?(**params.merge(from_list_id: list1.id)) }
- .to raise_error(Gitlab::Graphql::Errors::ArgumentError,
- 'Both fromListId and toListId must be present'
+ context 'when required arguments are missing' do
+ let(:move_params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(
+ Gitlab::Graphql::Errors::ArgumentError,
+ "At least one of the arguments fromListId, toListId, afterId or beforeId is required"
)
+ end
+ end
+
+ context 'when the board ID is wrong' do
+ before do
+ params[:board_id] = global_id_of(project)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(::GraphQL::LoadApplicationObjectFailedError)
+ end
end
- end
- describe '#resolve' do
context 'when user have access to resources' do
it 'moves and repositions issue' do
subject
@@ -63,15 +83,11 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
end
end
- context 'when user have no access to resources' do
- shared_examples 'raises a resource not available error' do
- it { expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
- end
-
- context 'when user cannot update issue' do
- let(:current_user) { guest }
+ context 'when user cannot update issue' do
+ let(:current_ctx) { { current_user: guest } }
- it_behaves_like 'raises a resource not available error'
+ specify do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
diff --git a/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb b/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb
deleted file mode 100644
index 8d1fce406fa..00000000000
--- a/spec/graphql/mutations/concerns/mutations/can_mutate_spammable_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Mutations::CanMutateSpammable do
- let(:mutation_class) do
- Class.new(Mutations::BaseMutation) do
- include Mutations::CanMutateSpammable
- end
- end
-
- let(:request) { double(:request) }
- let(:query) { double(:query, schema: GitlabSchema) }
- let(:context) { GraphQL::Query::Context.new(query: query, object: nil, values: { request: request }) }
-
- subject(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
-
- describe '#additional_spam_params' do
- it 'returns additional spam-related params' do
- expect(subject.send(:additional_spam_params)).to eq({ api: true, request: request })
- end
- end
-
- describe '#with_spam_action_fields' do
- let(:spam_log) { double(:spam_log, id: 1) }
- let(:spammable) { double(:spammable, spam?: true, render_recaptcha?: true, spam_log: spam_log) }
-
- before do
- allow(Gitlab::CurrentSettings).to receive(:recaptcha_site_key) { 'abc123' }
- end
-
- it 'merges in spam action fields from spammable' do
- result = subject.send(:with_spam_action_response_fields, spammable) do
- { other_field: true }
- end
- expect(result)
- .to eq({
- spam: true,
- needs_captcha_response: true,
- spam_log_id: 1,
- captcha_site_key: 'abc123',
- other_field: true
- })
- end
- end
-end
diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb
index 326d88cea80..ada88b7652c 100644
--- a/spec/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/graphql/mutations/design_management/upload_spec.rb
@@ -32,6 +32,10 @@ RSpec.describe Mutations::DesignManagement::Upload do
end
context "when the feature is not available" do
+ before do
+ enable_design_management(false)
+ end
+
it_behaves_like "resource not available"
end
@@ -52,10 +56,10 @@ RSpec.describe Mutations::DesignManagement::Upload do
.map { |f| RenameableUpload.unique_file(f) }
end
- def creates_designs
+ def creates_designs(&block)
prior_count = DesignManagement::Design.count
- expect { yield }.not_to raise_error
+ expect(&block).not_to raise_error
expect(DesignManagement::Design.count).to eq(prior_count + files.size)
end
@@ -99,20 +103,20 @@ RSpec.describe Mutations::DesignManagement::Upload do
it_behaves_like "resource not available"
end
- context "a valid design" do
+ context "with a valid design" do
it "returns the updated designs" do
expect(resolve[:errors]).to eq []
expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png")
end
end
- context "context when passing an invalid project" do
+ context "when passing an invalid project" do
let(:project) { build(:project) }
it_behaves_like "resource not available"
end
- context "context when passing an invalid issue" do
+ context "when passing an invalid issue" do
let(:issue) { build(:issue) }
it_behaves_like "resource not available"
diff --git a/spec/graphql/mutations/issues/set_assignees_spec.rb b/spec/graphql/mutations/issues/set_assignees_spec.rb
index 9a27c5acdac..4cc49e76bc6 100644
--- a/spec/graphql/mutations/issues/set_assignees_spec.rb
+++ b/spec/graphql/mutations/issues/set_assignees_spec.rb
@@ -11,7 +11,12 @@ RSpec.describe Mutations::Issues::SetAssignees do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
- subject { mutation.resolve(project_path: issue.project.full_path, iid: issue.iid, assignee_usernames: [assignee.username]) }
+ subject do
+ mutation.resolve(project_path: issue.project.full_path,
+ iid: issue.iid,
+ operation_mode: Types::MutationOperationModeEnum.default_mode,
+ assignee_usernames: [assignee.username])
+ end
it_behaves_like 'permission level for issue mutation is correctly verified'
end
diff --git a/spec/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb
index e2eab591341..9b0460bc709 100644
--- a/spec/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -11,7 +11,12 @@ RSpec.describe Mutations::MergeRequests::SetAssignees do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
- subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: [assignee.username]) }
+ subject do
+ mutation.resolve(project_path: merge_request.project.full_path,
+ iid: merge_request.iid,
+ operation_mode: described_class.arguments['operationMode'].default_value,
+ assignee_usernames: [assignee.username])
+ end
it_behaves_like 'permission level for merge request mutation is correctly verified'
end
diff --git a/spec/graphql/mutations/release_asset_links/delete_spec.rb b/spec/graphql/mutations/release_asset_links/delete_spec.rb
new file mode 100644
index 00000000000..15d320b58ee
--- /dev/null
+++ b/spec/graphql/mutations/release_asset_links/delete_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::ReleaseAssetLinks::Delete do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be_with_reload(:release) { create(:release, project: project) }
+ let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+ let_it_be_with_reload(:release_link) { create(:release_link, release: release) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+ let(:mutation_arguments) { { id: release_link.to_global_id } }
+
+ describe '#resolve' do
+ subject(:resolve) do
+ mutation.resolve(**mutation_arguments)
+ end
+
+ let(:deleted_link) { subject[:link] }
+
+ context 'when the current user has access to delete the link' do
+ let(:current_user) { maintainer }
+
+ it 'deletes the link and returns it', :aggregate_failures do
+ expect(deleted_link).to eq(release_link)
+
+ expect(release.links).to be_empty
+ end
+
+ context "when the link doesn't exist" do
+ let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context "when the provided ID is invalid" do
+ let(:mutation_arguments) { super().merge(id: 'not-a-valid-gid') }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ end
+ end
+ end
+
+ context 'when the current user does not have access to delete the link' do
+ let(:current_user) { developer }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/release_asset_links/update_spec.rb b/spec/graphql/mutations/release_asset_links/update_spec.rb
index 065089066f1..20c1c8b581c 100644
--- a/spec/graphql/mutations/release_asset_links/update_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/update_spec.rb
@@ -166,7 +166,7 @@ RSpec.describe Mutations::ReleaseAssetLinks::Update do
end
context "when the link doesn't exist" do
- let(:mutation_arguments) { super().merge(id: 'gid://gitlab/Releases::Link/999999') }
+ let(:mutation_arguments) { super().merge(id: "gid://gitlab/Releases::Link/#{non_existing_record_id}") }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
diff --git a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
index 2cd61dd7bcf..a4d1101bc4f 100644
--- a/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management/http_integrations_resolver_spec.rb
@@ -14,7 +14,9 @@ RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do
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) }
+ let(:params) { {} }
+
+ subject { sync(resolve_http_integrations(params)) }
before do
project.add_developer(developer)
@@ -41,11 +43,25 @@ RSpec.describe Resolvers::AlertManagement::HttpIntegrationsResolver do
let(:current_user) { maintainer }
it { is_expected.to contain_exactly(active_http_integration) }
+
+ context 'when HTTP Integration ID is given' do
+ context 'when integration is from the current project' do
+ let(:params) { { id: global_id_of(inactive_http_integration) } }
+
+ it { is_expected.to contain_exactly(inactive_http_integration) }
+ end
+
+ context 'when integration is from other project' do
+ let(:params) { { id: global_id_of(other_proj_integration) } }
+
+ it { is_expected.to be_empty }
+ end
+ end
end
private
def resolve_http_integrations(args = {}, context = { current_user: current_user })
- resolve(described_class, obj: project, ctx: context)
+ resolve(described_class, obj: project, args: args, ctx: context)
end
end
diff --git a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
index 36e409e0677..fb0fb6729d4 100644
--- a/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
+++ b/spec/graphql/resolvers/alert_management/integrations_resolver_spec.rb
@@ -7,12 +7,16 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
+ let_it_be(:project2) { 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) }
+ let_it_be(:other_proj_integration) { create(:alert_management_http_integration, project: project2) }
+ let_it_be(:other_proj_prometheus_integration) { create(:prometheus_service, project: project2) }
- subject { sync(resolve_http_integrations) }
+ let(:params) { {} }
+
+ subject { sync(resolve_http_integrations(params)) }
specify do
expect(described_class).to have_nullable_graphql_type(Types::AlertManagement::IntegrationType.connection_type)
@@ -25,14 +29,43 @@ RSpec.describe Resolvers::AlertManagement::IntegrationsResolver do
context 'user has permission' do
before do
project.add_maintainer(current_user)
+ project2.add_maintainer(current_user)
end
it { is_expected.to contain_exactly(active_http_integration, prometheus_integration) }
+
+ context 'when HTTP Integration ID is given' do
+ context 'when integration is from the current project' do
+ let(:params) { { id: global_id_of(inactive_http_integration) } }
+
+ it { is_expected.to contain_exactly(inactive_http_integration) }
+ end
+
+ context 'when integration is from other project' do
+ let(:params) { { id: global_id_of(other_proj_integration) } }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'when Prometheus Integration ID is given' do
+ context 'when integration is from the current project' do
+ let(:params) { { id: global_id_of(prometheus_integration) } }
+
+ it { is_expected.to contain_exactly(prometheus_integration) }
+ end
+
+ context 'when integration is from other project' do
+ let(:params) { { id: global_id_of(other_proj_prometheus_integration) } }
+
+ it { is_expected.to be_empty }
+ end
+ end
end
private
def resolve_http_integrations(args = {}, context = { current_user: current_user })
- resolve(described_class, obj: project, ctx: context)
+ resolve(described_class, obj: project, args: args, ctx: context)
end
end
diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb
new file mode 100644
index 00000000000..bc0344796ee
--- /dev/null
+++ b/spec/graphql/resolvers/blobs_resolver_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::BlobsResolver do
+ include GraphqlHelpers
+
+ describe '.resolver_complexity' do
+ it 'adds one per path being resolved' do
+ control = described_class.resolver_complexity({}, child_complexity: 1)
+
+ expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1))
+ .to eq(control + 3)
+ end
+ end
+
+ describe '#resolve' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:repository) { project.repository }
+ let(:args) { { paths: paths, ref: ref } }
+ let(:paths) { [] }
+ let(:ref) { nil }
+
+ subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) }
+
+ context 'when unauthorized' do
+ it 'raises an exception' do
+ expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'using no filter' do
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'using paths filter' do
+ let(:paths) { ['README.md'] }
+
+ it 'returns the specified blobs for HEAD' do
+ is_expected.to contain_exactly(have_attributes(path: 'README.md'))
+ end
+
+ context 'specifying a non-existent blob' do
+ let(:paths) { ['non-existent'] }
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'specifying a different ref' do
+ let(:ref) { 'add-pdf-file' }
+ let(:paths) { ['files/pdf/test.pdf', 'README.md'] }
+
+ it 'returns the specified blobs for that ref' do
+ is_expected.to contain_exactly(
+ have_attributes(path: 'files/pdf/test.pdf'),
+ have_attributes(path: 'README.md')
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
index 5eda840854a..6ffc8b045e9 100644
--- a/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/board_list_issues_resolver_spec.rb
@@ -39,6 +39,24 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
expect(result).to match_array([issue1])
end
+
+ it 'raises an exception if both assignee_username and assignee_wildcard_id are present' do
+ expect do
+ resolve_board_list_issues(args: { filters: { assignee_username: ['username'], assignee_wildcard_id: 'NONE' } })
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+
+ it 'accepts assignee wildcard id NONE' do
+ result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'NONE' } })
+
+ expect(result).to match_array([issue1, issue2, issue3])
+ end
+
+ it 'accepts assignee wildcard id ANY' do
+ result = resolve_board_list_issues(args: { filters: { assignee_wildcard_id: 'ANY' } })
+
+ expect(result).to match_array([])
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
index c44f6b623d7..1b69bf7f63a 100644
--- a/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/jobs_resolver_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe Resolvers::Ci::JobsResolver do
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)
+ create(:ci_build, name: 'Job with tags', pipeline: pipeline, tag_list: ['review'])
end
describe '#resolve' do
@@ -24,7 +25,8 @@ RSpec.describe Resolvers::Ci::JobsResolver do
have_attributes(name: 'Normal job'),
have_attributes(name: 'DAST job'),
have_attributes(name: 'SAST job'),
- have_attributes(name: 'Container scanning job')
+ have_attributes(name: 'Container scanning job'),
+ have_attributes(name: 'Job with tags')
)
end
end
@@ -43,5 +45,18 @@ RSpec.describe Resolvers::Ci::JobsResolver do
)
end
end
+
+ context 'when a job has tags' do
+ it "returns jobs with tags when applicable" do
+ jobs = resolve(described_class, obj: pipeline)
+ expect(jobs).to contain_exactly(
+ have_attributes(tag_list: []),
+ have_attributes(tag_list: []),
+ have_attributes(tag_list: []),
+ have_attributes(tag_list: []),
+ have_attributes(tag_list: ['review'])
+ )
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
index 1eb6f363d5b..3cb6e94e81e 100644
--- a/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_platforms_resolver_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Resolvers::Ci::RunnerPlatformsResolver do
subject(:resolve_subject) { resolve(described_class) }
it 'returns all possible runner platforms' do
- expect(resolve_subject).to include(
+ expect(resolve_subject).to contain_exactly(
hash_including(name: :linux), hash_including(name: :osx),
hash_including(name: :windows), hash_including(name: :docker),
hash_including(name: :kubernetes)
diff --git a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
index 3d004290d9b..13ef89023d9 100644
--- a/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/runner_setup_resolver_spec.rb
@@ -8,12 +8,11 @@ RSpec.describe Resolvers::Ci::RunnerSetupResolver do
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)) }
+ subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: platform, architecture: 'amd64' }) }
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)
@@ -27,77 +26,9 @@ RSpec.describe Resolvers::Ci::RunnerSetupResolver do
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
+ 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
diff --git a/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb b/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb
new file mode 100644
index 00000000000..e78bd06b567
--- /dev/null
+++ b/spec/graphql/resolvers/ci/test_report_summary_resolver_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::TestReportSummaryResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+
+ subject(:resolve_subject) { resolve(described_class, obj: pipeline) }
+
+ context 'when pipeline has build report results' do
+ let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) }
+
+ it 'returns test report summary data' do
+ expect(resolve_subject.keys).to contain_exactly(:total, :test_suites)
+ expect(resolve_subject[:test_suites][0].keys).to contain_exactly(:build_ids, :name, :total_time, :total_count, :success_count, :failed_count, :skipped_count, :error_count, :suite_error)
+ expect(resolve_subject[:total][:time]).to eq(0.42)
+ expect(resolve_subject[:total][:count]).to eq(2)
+ expect(resolve_subject[:total][:success]).to eq(0)
+ expect(resolve_subject[:total][:failed]).to eq(0)
+ expect(resolve_subject[:total][:skipped]).to eq(0)
+ expect(resolve_subject[:total][:error]).to eq(2)
+ expect(resolve_subject[:total][:suite_error]).to eq(nil)
+ end
+ end
+
+ context 'when pipeline does not have build report results' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'renders test report summary data' do
+ expect(resolve_subject.keys).to contain_exactly(:total, :test_suites)
+ expect(resolve_subject[:test_suites]).to eq([])
+ expect(resolve_subject[:total][:time]).to eq(0)
+ expect(resolve_subject[:total][:count]).to eq(0)
+ expect(resolve_subject[:total][:success]).to eq(0)
+ expect(resolve_subject[:total][:failed]).to eq(0)
+ expect(resolve_subject[:total][:skipped]).to eq(0)
+ expect(resolve_subject[:total][:error]).to eq(0)
+ expect(resolve_subject[:total][:suite_error]).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
new file mode 100644
index 00000000000..606c6eb03a3
--- /dev/null
+++ b/spec/graphql/resolvers/ci/test_suite_resolver_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Ci::TestSuiteResolver do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+
+ describe '#resolve' do
+ subject(:test_suite) { resolve(described_class, obj: pipeline, args: { build_ids: build_ids }) }
+
+ context 'when pipeline has builds with test reports' do
+ let_it_be(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') }
+
+ let(:suite_name) { 'test' }
+ let(:build_ids) { pipeline.latest_builds.pluck(:id) }
+
+ before do
+ build = main_pipeline.builds.last
+ build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window
+
+ # The JUnit fixture for the given build has 3 failures.
+ # This service will create 1 test case failure record for each.
+ Ci::TestFailureHistoryService.new(main_pipeline).execute
+ end
+
+ it 'renders test suite data' do
+ expect(test_suite[:name]).to eq('test')
+
+ # Each test failure in this pipeline has a matching failure in the default branch
+ recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] }
+ expect(recent_failures).to eq([
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' },
+ { count: 1, base_branch: 'master' }
+ ])
+ end
+ end
+
+ context 'when pipeline has no builds that matches the given build_ids' do
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:suite_name) { 'test' }
+ let(:build_ids) { [non_existing_record_id] }
+
+ it 'returns nil' do
+ expect(test_suite).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
index 27ac1572cab..4c244da5c62 100644
--- a/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
+++ b/spec/graphql/resolvers/concerns/looks_ahead_spec.rb
@@ -38,11 +38,8 @@ RSpec.describe LooksAhead do
user = Class.new(GraphQL::Schema::Object) do
graphql_name 'User'
field :name, String, null: true
- field :issues, issue.connection_type,
- null: true
- field :issues_with_lookahead, issue.connection_type,
- resolver: issues_resolver,
- null: true
+ field :issues, issue.connection_type, null: true
+ field :issues_with_lookahead, issue.connection_type, resolver: issues_resolver, null: true
end
Class.new(GraphQL::Schema) do
@@ -101,7 +98,7 @@ RSpec.describe LooksAhead do
expect(res['errors']).to be_blank
expect(res.dig('data', 'findUser', 'name')).to eq(the_user.name)
- %w(issues issuesWithLookahead).each do |field|
+ %w[issues issuesWithLookahead].each do |field|
expect(all_issue_titles(res, field)).to match_array(issue_titles)
expect(all_label_ids(res, field)).to match_array(expected_label_ids)
end
diff --git a/spec/graphql/resolvers/group_milestones_resolver_spec.rb b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
index d8ff8e9c1f2..dd3f1676538 100644
--- a/spec/graphql/resolvers/group_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
@@ -136,5 +136,56 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3])
end
end
+
+ describe 'include_descendants and include_ancestors' do
+ let_it_be(:parent_group) { create(:group, :public) }
+ let_it_be(:group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:accessible_group) { create(:group, :private, parent: group) }
+ let_it_be(:accessible_project) { create(:project, group: accessible_group) }
+ let_it_be(:inaccessible_group) { create(:group, :private, parent: group) }
+ let_it_be(:inaccessible_project) { create(:project, :private, group: group) }
+ let_it_be(:milestone1) { create(:milestone, group: group) }
+ let_it_be(:milestone2) { create(:milestone, group: accessible_group) }
+ let_it_be(:milestone3) { create(:milestone, project: accessible_project) }
+ let_it_be(:milestone4) { create(:milestone, group: inaccessible_group) }
+ let_it_be(:milestone5) { create(:milestone, project: inaccessible_project) }
+ let_it_be(:milestone6) { create(:milestone, group: parent_group) }
+
+ before do
+ accessible_group.add_developer(current_user)
+ end
+
+ context 'when including neither ancestor or descendant milestones in a public group' do
+ let(:args) { {} }
+
+ it 'finds milestones only in accessible projects and groups' do
+ expect(resolve_group_milestones(args)).to match_array([milestone1])
+ end
+ end
+
+ context 'when including descendant milestones in a public group' do
+ let(:args) { { include_descendants: true } }
+
+ it 'finds milestones only in accessible projects and groups' do
+ expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3])
+ end
+ end
+
+ context 'when including ancestor milestones in a public group' do
+ let(:args) { { include_ancestors: true } }
+
+ it 'finds milestones only in accessible projects and groups' do
+ expect(resolve_group_milestones(args)).to match_array([milestone1, milestone6])
+ end
+ end
+
+ context 'when including both ancestor or descendant milestones in a public group' do
+ let(:args) { { include_descendants: true, include_ancestors: true } }
+
+ it 'finds milestones only in accessible projects and groups' do
+ expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3, milestone6])
+ end
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
index decc3569d6c..3fbd9bd2368 100644
--- a/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
+++ b/spec/graphql/resolvers/issue_status_counts_resolver_spec.rb
@@ -69,6 +69,14 @@ RSpec.describe Resolvers::IssueStatusCountsResolver do
expect(result.closed).to eq 1
end
+ context 'when both assignee_username and assignee_usernames are provided' do
+ it 'raises a mutually exclusive filter error' do
+ expect do
+ resolve_issue_status_counts(assignee_usernames: [current_user.username], assignee_username: current_user.username)
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
+ end
+ end
+
private
def resolve_issue_status_counts(args = {}, context = { current_user: current_user })
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 6e802bf7d25..7c2ceb50066 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -46,10 +46,6 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(milestone_title: [milestone.title])).to contain_exactly(issue1)
end
- it 'filters by assignee_username' do
- expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2)
- end
-
it 'filters by two assignees' do
assignee2 = create(:user)
issue2.update!(assignees: [assignee, assignee2])
@@ -78,6 +74,24 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
+ describe 'filters by assignee_username' do
+ it 'filters by assignee_username' do
+ expect(resolve_issues(assignee_username: [assignee.username])).to contain_exactly(issue2)
+ end
+
+ it 'filters by assignee_usernames' do
+ expect(resolve_issues(assignee_usernames: [assignee.username])).to contain_exactly(issue2)
+ end
+
+ context 'when both assignee_username and assignee_usernames are provided' do
+ it 'raises a mutually exclusive filter error' do
+ expect do
+ resolve_issues(assignee_usernames: [assignee.username], assignee_username: assignee.username)
+ end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
+ end
+ end
+ end
+
describe 'filters by created_at' do
it 'filters by created_before' do
expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
@@ -144,6 +158,29 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
+ describe 'filters by negated params' do
+ it 'returns issues without the specified iids' do
+ expect(resolve_issues(not: { iids: [issue1.iid] })).to contain_exactly(issue2)
+ end
+
+ it 'returns issues without the specified label names' do
+ expect(resolve_issues(not: { label_name: [label1.title] })).to be_empty
+ expect(resolve_issues(not: { label_name: [label2.title] })).to contain_exactly(issue1)
+ end
+
+ it 'returns issues without the specified milestone' do
+ expect(resolve_issues(not: { milestone_title: [milestone.title] })).to contain_exactly(issue2)
+ end
+
+ it 'returns issues without the specified assignee_usernames' do
+ expect(resolve_issues(not: { assignee_usernames: [assignee.username] })).to contain_exactly(issue1)
+ end
+
+ it 'returns issues without the specified assignee_id' do
+ expect(resolve_issues(not: { assignee_id: [assignee.id] })).to contain_exactly(issue1)
+ end
+ end
+
describe 'sorting' do
context 'when sorting by created' do
it 'sorts issues ascending' do
diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 7dd968d90a8..aec6c6c6708 100644
--- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
include SortingHelper
let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:other_user) { create(:user) }
@@ -16,10 +17,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
let_it_be(:merge_request_3) { create(:merge_request, :unique_branches, **common_attrs) }
let_it_be(:merge_request_4) { create(:merge_request, :unique_branches, :locked, **common_attrs) }
let_it_be(:merge_request_5) { create(:merge_request, :simple, :locked, **common_attrs) }
- let_it_be(:merge_request_6) { create(:labeled_merge_request, :unique_branches, labels: create_list(:label, 2, project: project), **common_attrs) }
- let_it_be(:merge_request_with_milestone) { create(:merge_request, :unique_branches, **common_attrs, milestone: milestone) }
- let_it_be(:other_project) { create(:project, :repository) }
- let_it_be(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
+ let_it_be(:merge_request_6) do
+ create(:labeled_merge_request, :unique_branches, **common_attrs, labels: create_list(:label, 2, project: project))
+ end
+
+ let_it_be(:merge_request_with_milestone) do
+ create(:merge_request, :unique_branches, **common_attrs, milestone: milestone)
+ end
+
+ let_it_be(:other_merge_request) do
+ create(:merge_request, source_project: other_project, target_project: other_project)
+ end
let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid }
@@ -41,13 +49,16 @@ RSpec.describe Resolvers::MergeRequestsResolver do
# AND "merge_requests"."iid" = 1 ORDER BY "merge_requests"."id" DESC
# SELECT "projects".* FROM "projects" WHERE "projects"."id" = 2
# SELECT "project_features".* FROM "project_features" WHERE "project_features"."project_id" = 2
- let(:queries_per_project) { 3 }
+ let(:queries_per_project) { 4 }
- context 'no arguments' do
+ context 'without arguments' do
it 'returns all merge requests' do
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)
+ 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
@@ -57,7 +68,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by iid alone' do
+ context 'with iid alone' do
it 'batch-resolves by target project full path and individual IID', :request_store do
# 1 query for project_authorizations, and 1 for merge_requests
result = batch_sync(max_queries: queries_per_project) do
@@ -83,7 +94,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
end
- it 'can batch-resolve merge requests from different projects', :request_store, :use_clean_rails_memory_store_caching do
+ it 'can batch-resolve merge requests from different projects', :request_store do
# 2 queries for project_authorizations, and 2 for merge_requests
results = batch_sync(max_queries: queries_per_project * 2) do
a = resolve_mr(project, iids: [iid_1])
@@ -121,7 +132,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by source branches' do
+ context 'with source branches argument' do
it 'takes one argument' do
result = resolve_mr(project, source_branches: [merge_request_3.source_branch])
@@ -131,13 +142,13 @@ RSpec.describe Resolvers::MergeRequestsResolver do
it 'takes more than one argument' do
mrs = [merge_request_3, merge_request_4]
branches = mrs.map(&:source_branch)
- result = resolve_mr(project, source_branches: branches )
+ result = resolve_mr(project, source_branches: branches)
expect(result).to match_array(mrs)
end
end
- context 'by target branches' do
+ context 'with target branches argument' do
it 'takes one argument' do
result = resolve_mr(project, target_branches: [merge_request_3.target_branch])
@@ -153,7 +164,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by state' do
+ context 'with state argument' do
it 'takes one argument' do
result = resolve_mr(project, state: 'locked')
@@ -161,7 +172,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by label' do
+ context 'with label argument' do
let_it_be(:label) { merge_request_6.labels.first }
let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
@@ -178,7 +189,18 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by merged_after and merged_before' do
+ context 'with negated label argument' do
+ let_it_be(:label) { merge_request_6.labels.first }
+ let_it_be(:with_label) { create(:labeled_merge_request, :closed, labels: [label], **common_attrs) }
+
+ it 'excludes merge requests with given label from selection' do
+ result = resolve_mr(project, not: { labels: [label.title] })
+
+ expect(result).not_to include(merge_request_6, with_label)
+ end
+ end
+
+ context 'with merged_after and merged_before arguments' do
before do
merge_request_1.metrics.update!(merged_at: 10.days.ago)
end
@@ -196,7 +218,7 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
- context 'by milestone' do
+ context 'with milestone argument' do
it 'filters merge requests by milestone title' do
result = resolve_mr(project, milestone_title: milestone.title)
@@ -210,9 +232,17 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
end
+ context 'with negated milestone argument' do
+ it 'filters out merge requests with given milestone title' do
+ result = resolve_mr(project, not: { milestone_title: milestone.title })
+
+ expect(result).not_to include(merge_request_with_milestone)
+ end
+ end
+
describe 'combinations' do
it 'requires all filters' do
- create(:merge_request, :closed, source_project: project, target_project: project, source_branch: merge_request_4.source_branch)
+ create(:merge_request, :closed, **common_attrs, source_branch: merge_request_4.source_branch)
result = resolve_mr(project, source_branches: [merge_request_4.source_branch], state: 'locked')
diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
index 147a02e1d79..618d012bd6d 100644
--- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
+++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
@@ -112,7 +112,7 @@ RSpec.describe Resolvers::NamespaceProjectsResolver do
subject(:projects) { resolve_projects(args) }
let(:include_subgroups) { false }
- let(:project_3) { create(:project, name: 'Project', path: 'project', namespace: namespace) }
+ let!(:project_3) { create(:project, name: 'Project', path: 'project', namespace: namespace) }
context 'when ids is provided' do
let(:ids) { [project_3.to_global_id.to_s] }
diff --git a/spec/graphql/resolvers/project_jobs_resolver_spec.rb b/spec/graphql/resolvers/project_jobs_resolver_spec.rb
new file mode 100644
index 00000000000..94df2999163
--- /dev/null
+++ b/spec/graphql/resolvers/project_jobs_resolver_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::ProjectJobsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:irrelevant_project) { create(:project, :repository) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:irrelevant_pipeline) { create(:ci_pipeline, project: irrelevant_project) }
+ let_it_be(:build_one) { create(:ci_build, :success, name: 'Build One', pipeline: pipeline) }
+ let_it_be(:build_two) { create(:ci_build, :success, name: 'Build Two', pipeline: pipeline) }
+ let_it_be(:build_three) { create(:ci_build, :failed, name: 'Build Three', pipeline: pipeline) }
+
+ let(:irrelevant_build) { create(:ci_build, name: 'Irrelevant Build', pipeline: irrelevant_pipeline)}
+ let(:args) { {} }
+ let(:current_user) { create(:user) }
+
+ subject { resolve_jobs(args) }
+
+ describe '#resolve' do
+ context 'with authorized user' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'with statuses argument' do
+ let(:args) { { statuses: [Types::Ci::JobStatusEnum.coerce_isolated_input('SUCCESS')] } }
+
+ it { is_expected.to contain_exactly(build_one, build_two) }
+ end
+
+ context 'without statuses argument' do
+ it { is_expected.to contain_exactly(build_one, build_two, build_three) }
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ private
+
+ def resolve_jobs(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
index 69127c4b061..3d33e0b500d 100644
--- a/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_pipeline_resolver_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, iid: '1234', sha: 'sha') }
+ let_it_be(:other_project_pipeline) { create(:ci_pipeline, project: project, iid: '1235', sha: 'sha2') }
let_it_be(:other_pipeline) { create(:ci_pipeline) }
let(:current_user) { create(:user) }
@@ -23,6 +24,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed iid' do
+ expect(Ci::PipelinesFinder)
+ .to receive(:new)
+ .with(project, current_user, iids: ['1234'])
+ .and_call_original
+
result = batch_sync do
resolve_pipeline(project, { iid: '1234' })
end
@@ -31,6 +37,11 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'resolves pipeline for the passed sha' do
+ expect(Ci::PipelinesFinder)
+ .to receive(:new)
+ .with(project, current_user, sha: ['sha'])
+ .and_call_original
+
result = batch_sync do
resolve_pipeline(project, { sha: 'sha' })
end
@@ -39,8 +50,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for iid' do
- create(:ci_pipeline, project: project, iid: '1235')
-
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { iid: '1234' }) }
end
@@ -54,8 +63,6 @@ RSpec.describe Resolvers::ProjectPipelineResolver do
end
it 'keeps the queries under the threshold for sha' do
- create(:ci_pipeline, project: project, sha: 'sha2')
-
control = ActiveRecord::QueryRecorder.new do
batch_sync { resolve_pipeline(project, { sha: 'sha' }) }
end
diff --git a/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb
new file mode 100644
index 00000000000..398dd7a2e2e
--- /dev/null
+++ b/spec/graphql/resolvers/repository_branch_names_resolver_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::RepositoryBranchNamesResolver do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+
+ describe '#resolve' do
+ subject(:resolve_branch_names) do
+ resolve(
+ described_class,
+ obj: project.repository,
+ args: { search_pattern: pattern },
+ ctx: { current_user: project.creator }
+ )
+ end
+
+ context 'with empty search pattern' do
+ let(:pattern) { '' }
+
+ it 'returns nil' do
+ expect(resolve_branch_names).to eq(nil)
+ end
+ end
+
+ context 'with a valid search pattern' do
+ let(:pattern) { 'mas*' }
+
+ it 'returns matching branches' do
+ expect(resolve_branch_names).to match_array(['master'])
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb
new file mode 100644
index 00000000000..585cd657e35
--- /dev/null
+++ b/spec/graphql/resolvers/timelog_resolver_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::TimelogResolver do
+ include GraphqlHelpers
+
+ specify do
+ expect(described_class).to have_non_null_graphql_type(::Types::TimelogType.connection_type)
+ end
+
+ context "with a group" do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+
+ before_all do
+ group.add_developer(current_user)
+ project.add_developer(current_user)
+ end
+
+ before do
+ group.clear_memoization(:timelogs)
+ end
+
+ describe '#resolve' do
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:issue2) { create(:issue, project: project) }
+ let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) }
+ let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) }
+ let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) }
+
+ let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } }
+
+ it 'finds all timelogs within given dates' do
+ timelogs = resolve_timelogs(**args)
+
+ expect(timelogs).to contain_exactly(timelog1)
+ end
+
+ it 'return nothing when user has insufficient permissions' do
+ user = create(:user)
+ group.add_guest(current_user)
+
+ expect(resolve_timelogs(user: user, **args)).to be_empty
+ end
+
+ context 'when start_time and end_date are present' do
+ let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } }
+
+ it 'finds timelogs until the end of day of end_date' do
+ timelogs = resolve_timelogs(**args)
+
+ expect(timelogs).to contain_exactly(timelog1, timelog2)
+ end
+ end
+
+ context 'when start_date and end_time are present' do
+ let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } }
+
+ it 'finds all timelogs within start_date and end_time' do
+ timelogs = resolve_timelogs(**args)
+
+ expect(timelogs).to contain_exactly(timelog1)
+ end
+ end
+
+ context 'when arguments are invalid' do
+ let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError }
+
+ context 'when no time or date arguments are present' do
+ let(:args) { {} }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Start and End arguments must be present/)
+ end
+ end
+
+ context 'when only start_time is present' do
+ let(:args) { { start_time: 6.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when only end_time is present' do
+ let(:args) { { end_time: 2.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when only start_date is present' do
+ let(:args) { { start_date: 6.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when only end_date is present' do
+ let(:args) { { end_date: 2.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when start_time and start_date are present' do
+ let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when end_time and end_date are present' do
+ let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Both Start and End arguments must be present/)
+ end
+ end
+
+ context 'when three arguments are present' do
+ let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Only Time or Date arguments must be present/)
+ end
+ end
+
+ context 'when start argument is after end argument' do
+ let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /Start argument must be before End argument/)
+ end
+ end
+
+ context 'when time range is more than 60 days' do
+ let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } }
+
+ it 'returns correct error' do
+ expect { resolve_timelogs(**args) }
+ .to raise_error(error_class, /The time range period cannot contain more than 60 days/)
+ end
+ end
+ end
+ end
+ end
+
+ def resolve_timelogs(user: current_user, **args)
+ context = { current_user: user }
+ resolve(described_class, obj: group, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/users/snippets_resolver_spec.rb b/spec/graphql/resolvers/users/snippets_resolver_spec.rb
index 11a5b7517e0..04fe3213a99 100644
--- a/spec/graphql/resolvers/users/snippets_resolver_spec.rb
+++ b/spec/graphql/resolvers/users/snippets_resolver_spec.rb
@@ -75,9 +75,19 @@ RSpec.describe Resolvers::Users::SnippetsResolver do
end.to raise_error(GraphQL::CoercionError)
end
end
+
+ context 'when user profile is private' do
+ it 'does not return snippets for that user' do
+ expect(resolve_snippets(obj: other_user)).to contain_exactly(other_personal_snippet, other_project_snippet)
+
+ other_user.update!(private_profile: true)
+
+ expect(resolve_snippets(obj: other_user)).to be_empty
+ end
+ end
end
- def resolve_snippets(args: {})
- resolve(described_class, args: args, ctx: { current_user: current_user }, obj: current_user)
+ def resolve_snippets(args: {}, context_user: current_user, obj: current_user)
+ resolve(described_class, args: args, ctx: { current_user: context_user }, obj: obj)
end
end
diff --git a/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb b/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb
index c50092d7f0e..d1c2b4044c1 100644
--- a/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb
+++ b/spec/graphql/types/admin/analytics/usage_trends/measurement_type_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe GitlabSchema.types['UsageTrendsMeasurement'] do
describe 'authorization' do
let_it_be(:measurement) { create(:usage_trends_measurement, :project_count) }
+
let(:user) { create(:user) }
let(:query) do
@@ -44,7 +45,7 @@ RSpec.describe GitlabSchema.types['UsageTrendsMeasurement'] do
let(:user) { create(:user, :admin) }
before do
- stub_feature_flags(user_mode_in_session: false)
+ stub_application_setting(admin_mode: false)
end
it 'returns data' do
diff --git a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
index b10c2a2ab2a..d057afb331c 100644
--- a/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
+++ b/spec/graphql/types/alert_management/prometheus_integration_type_spec.rb
@@ -48,15 +48,21 @@ RSpec.describe GitlabSchema.types['AlertManagementPrometheusIntegration'] do
end
end
- context 'without project' do
- let_it_be(:integration) { create(:prometheus_service, project: nil, group: create(:group)) }
-
- it_behaves_like 'has field with value', 'token' do
- let(:value) { nil }
- end
-
- it_behaves_like 'has field with value', 'url' do
- let(:value) { nil }
+ describe 'a group integration' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:integration) { create(:prometheus_service, project: nil, group: group) }
+
+ # Since it is impossible to authorize the parent here, given that the
+ # project is nil, all fields should be redacted:
+
+ described_class.fields.each_key do |field_name|
+ context "field: #{field_name}" do
+ it 'is redacted' do
+ expect do
+ resolve_field(field_name, integration, current_user: user)
+ end.to raise_error(GraphqlHelpers::UnauthorizedObject)
+ end
+ end
end
end
end
diff --git a/spec/graphql/types/base_enum_spec.rb b/spec/graphql/types/base_enum_spec.rb
index 744aee40044..bab0278ee25 100644
--- a/spec/graphql/types/base_enum_spec.rb
+++ b/spec/graphql/types/base_enum_spec.rb
@@ -3,6 +3,38 @@
require 'spec_helper'
RSpec.describe Types::BaseEnum do
+ describe '.from_rails_enum' do
+ let(:enum_type) { Class.new(described_class) }
+ let(:template) { "The name is '%{name}', James %{name}." }
+
+ let(:enum) do
+ {
+ 'foo' => 1,
+ 'bar' => 2,
+ 'baz' => 100
+ }
+ end
+
+ it 'contructs the correct values' do
+ enum_type.from_rails_enum(enum, description: template)
+
+ expect(enum_type.values).to match(
+ 'FOO' => have_attributes(
+ description: "The name is 'foo', James foo.",
+ value: 'foo'
+ ),
+ 'BAR' => have_attributes(
+ description: "The name is 'bar', James bar.",
+ value: 'bar'
+ ),
+ 'BAZ' => have_attributes(
+ description: "The name is 'baz', James baz.",
+ value: 'baz'
+ )
+ )
+ end
+ end
+
describe '.declarative_enum' do
let(:use_name) { true }
let(:use_description) { true }
@@ -26,12 +58,15 @@ RSpec.describe Types::BaseEnum do
end
end
- subject(:set_declarative_enum) { enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description) }
+ subject(:set_declarative_enum) do
+ enum_type.declarative_enum(enum_module, use_name: use_name, use_description: use_description)
+ end
describe '#graphql_name' do
context 'when the use_name is `true`' do
it 'changes the graphql_name' do
- expect { set_declarative_enum }.to change { enum_type.graphql_name }.from('OriginalName').to('Name')
+ expect { set_declarative_enum }
+ .to change(enum_type, :graphql_name).from('OriginalName').to('Name')
end
end
@@ -39,7 +74,8 @@ RSpec.describe Types::BaseEnum do
let(:use_name) { false }
it 'does not change the graphql_name' do
- expect { set_declarative_enum }.not_to change { enum_type.graphql_name }.from('OriginalName')
+ expect { set_declarative_enum }
+ .not_to change(enum_type, :graphql_name).from('OriginalName')
end
end
end
@@ -47,7 +83,8 @@ RSpec.describe Types::BaseEnum do
describe '#description' do
context 'when the use_description is `true`' do
it 'changes the description' do
- expect { set_declarative_enum }.to change { enum_type.description }.from('Original description').to('Description')
+ expect { set_declarative_enum }
+ .to change(enum_type, :description).from('Original description').to('Description')
end
end
@@ -55,7 +92,8 @@ RSpec.describe Types::BaseEnum do
let(:use_description) { false }
it 'does not change the description' do
- expect { set_declarative_enum }.not_to change { enum_type.description }.from('Original description')
+ expect { set_declarative_enum }
+ .not_to change(enum_type, :description).from('Original description')
end
end
end
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
diff --git a/spec/graphql/types/board_type_spec.rb b/spec/graphql/types/board_type_spec.rb
index dca3cfd8aaf..403fbe1f290 100644
--- a/spec/graphql/types/board_type_spec.rb
+++ b/spec/graphql/types/board_type_spec.rb
@@ -8,8 +8,18 @@ RSpec.describe GitlabSchema.types['Board'] do
specify { expect(described_class).to require_graphql_authorizations(:read_issue_board) }
it 'has specific fields' do
- expected_fields = %w[id name web_url web_path]
+ expected_fields = %w[
+ id
+ name
+ hideBacklogList
+ hideClosedList
+ createdAt
+ updatedAt
+ lists
+ webPath
+ webUrl
+ ]
- expect(described_class).to include_graphql_fields(*expected_fields)
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
end
diff --git a/spec/graphql/types/boards/board_issue_input_type_spec.rb b/spec/graphql/types/boards/board_issue_input_type_spec.rb
index 6319ff9a88e..5d3efb9b40d 100644
--- a/spec/graphql/types/boards/board_issue_input_type_spec.rb
+++ b/spec/graphql/types/boards/board_issue_input_type_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['BoardIssueInput'] do
it { expect(described_class.graphql_name).to eq('BoardIssueInput') }
- it 'exposes negated issue arguments' do
+ it 'has specific fields' do
allowed_args = %w(labelName milestoneTitle assigneeUsername authorUsername
- releaseTag myReactionEmoji not search)
+ releaseTag myReactionEmoji not search assigneeWildcardId)
expect(described_class.arguments.keys).to include(*allowed_args)
expect(described_class.arguments['not'].type).to eq(Types::Boards::NegatedBoardIssueInputType)
diff --git a/spec/graphql/types/ci/job_status_enum_spec.rb b/spec/graphql/types/ci/job_status_enum_spec.rb
new file mode 100644
index 00000000000..e8a1a2e0aa8
--- /dev/null
+++ b/spec/graphql/types/ci/job_status_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiJobStatus'] do
+ it 'exposes all job status values' do
+ expect(described_class.values.values).to contain_exactly(
+ *::Ci::HasStatus::AVAILABLE_STATUSES.map do |status|
+ have_attributes(value: status, graphql_name: status.upcase)
+ end
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/job_type_spec.rb b/spec/graphql/types/ci/job_type_spec.rb
index 25f626cea0f..787e2174070 100644
--- a/spec/graphql/types/ci/job_type_spec.rb
+++ b/spec/graphql/types/ci/job_type_spec.rb
@@ -8,14 +8,32 @@ RSpec.describe Types::Ci::JobType do
it 'exposes the expected fields' do
expected_fields = %i[
- pipeline
+ active
+ allow_failure
+ artifacts
+ cancelable
+ commitPath
+ coverage
+ created_at
+ detailedStatus
+ duration
+ finished_at
+ id
name
needs
- detailedStatus
+ pipeline
+ playable
+ queued_at
+ refName
+ refPath
+ retryable
scheduledAt
- artifacts
- finished_at
- duration
+ schedulingType
+ shortSha
+ stage
+ started_at
+ status
+ tags
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index e0e84a1b635..c7d2cbdb765 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -11,8 +11,9 @@ RSpec.describe Types::Ci::PipelineType do
expected_fields = %w[
id iid sha before_sha status detailed_status config_source duration
coverage created_at updated_at started_at finished_at committed_at
- stages user retryable cancelable jobs source_job downstream
- upstream path project active user_permissions warnings commit_path
+ stages user retryable cancelable jobs source_job job downstream
+ upstream path project active user_permissions warnings commit_path uses_needs
+ test_report_summary test_suite
]
if Gitlab.ee?
diff --git a/spec/graphql/types/ci/recent_failures_type_spec.rb b/spec/graphql/types/ci/recent_failures_type_spec.rb
new file mode 100644
index 00000000000..38369da46bf
--- /dev/null
+++ b/spec/graphql/types/ci/recent_failures_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::RecentFailuresType do
+ specify { expect(described_class.graphql_name).to eq('RecentFailures') }
+
+ it 'contains attributes related to a recent failure history for a test case' do
+ expected_fields = %w[
+ count base_branch
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/stage_type_spec.rb b/spec/graphql/types/ci/stage_type_spec.rb
index 9a8d4fa96a3..cb8c1cb02cd 100644
--- a/spec/graphql/types/ci/stage_type_spec.rb
+++ b/spec/graphql/types/ci/stage_type_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Types::Ci::StageType do
name
groups
detailedStatus
+ jobs
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/ci/test_case_status_enum_spec.rb b/spec/graphql/types/ci/test_case_status_enum_spec.rb
new file mode 100644
index 00000000000..ba2d1aefb20
--- /dev/null
+++ b/spec/graphql/types/ci/test_case_status_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestCaseStatusEnum do
+ specify { expect(described_class.graphql_name).to eq('TestCaseStatus') }
+
+ it 'exposes all test case status types' do
+ expect(described_class.values.keys).to eq(
+ ::Gitlab::Ci::Reports::TestCase::STATUS_TYPES
+ )
+ end
+end
diff --git a/spec/graphql/types/ci/test_case_type_spec.rb b/spec/graphql/types/ci/test_case_type_spec.rb
new file mode 100644
index 00000000000..e6cd70c287e
--- /dev/null
+++ b/spec/graphql/types/ci/test_case_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestCaseType do
+ specify { expect(described_class.graphql_name).to eq('TestCase') }
+
+ it 'contains attributes related to a pipeline test case' do
+ expected_fields = %w[
+ name status classname file attachment_url execution_time stack_trace system_output recent_failures
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_report_summary_type_spec.rb b/spec/graphql/types/ci/test_report_summary_type_spec.rb
new file mode 100644
index 00000000000..06974da0b88
--- /dev/null
+++ b/spec/graphql/types/ci/test_report_summary_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestReportSummaryType do
+ specify { expect(described_class.graphql_name).to eq('TestReportSummary') }
+
+ it 'contains attributes related to a pipeline test report summary' do
+ expected_fields = %w[
+ total test_suites
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_report_total_type_spec.rb b/spec/graphql/types/ci/test_report_total_type_spec.rb
new file mode 100644
index 00000000000..e5b7b358edb
--- /dev/null
+++ b/spec/graphql/types/ci/test_report_total_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestReportTotalType do
+ specify { expect(described_class.graphql_name).to eq('TestReportTotal') }
+
+ it 'contains attributes related to a pipeline test report summary' do
+ expected_fields = %w[
+ time count success failed skipped error suite_error
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_suite_summary_type_spec.rb b/spec/graphql/types/ci/test_suite_summary_type_spec.rb
new file mode 100644
index 00000000000..e87782037c7
--- /dev/null
+++ b/spec/graphql/types/ci/test_suite_summary_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestSuiteSummaryType do
+ specify { expect(described_class.graphql_name).to eq('TestSuiteSummary') }
+
+ it 'contains attributes related to a pipeline test report summary' do
+ expected_fields = %w[
+ name total_time total_count success_count failed_count skipped_count error_count suite_error build_ids
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/test_suite_type_spec.rb b/spec/graphql/types/ci/test_suite_type_spec.rb
new file mode 100644
index 00000000000..d9caca3e2c3
--- /dev/null
+++ b/spec/graphql/types/ci/test_suite_type_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::TestSuiteType do
+ specify { expect(described_class.graphql_name).to eq('TestSuite') }
+
+ it 'contains attributes related to a pipeline test suite' do
+ expected_fields = %w[
+ name total_time total_count success_count failed_count skipped_count error_count suite_error test_cases
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/global_id_type_spec.rb b/spec/graphql/types/global_id_type_spec.rb
index 8eb023ad2a3..4df51dc8d1b 100644
--- a/spec/graphql/types/global_id_type_spec.rb
+++ b/spec/graphql/types/global_id_type_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Types::GlobalIDType do
let_it_be(:project) { create(:project) }
+
let(:gid) { project.to_global_id }
it 'is has the correct name' do
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 21fc530149c..6908a610aae 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
- create_note_email]
+ create_note_email timelogs]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/merge_request_review_state_enum_spec.rb b/spec/graphql/types/merge_request_review_state_enum_spec.rb
new file mode 100644
index 00000000000..486e1c4f502
--- /dev/null
+++ b/spec/graphql/types/merge_request_review_state_enum_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeRequestReviewState'] do
+ it 'the correct enum members' do
+ expect(described_class.values).to match(
+ 'REVIEWED' => have_attributes(
+ description: 'The merge request is reviewed.',
+ value: 'reviewed'
+ ),
+ 'UNREVIEWED' => have_attributes(
+ description: 'The merge request is unreviewed.',
+ value: 'unreviewed'
+ )
+ )
+ end
+end
diff --git a/spec/graphql/types/merge_requests/reviewer_type_spec.rb b/spec/graphql/types/merge_requests/reviewer_type_spec.rb
new file mode 100644
index 00000000000..c2182e9968c
--- /dev/null
+++ b/spec/graphql/types/merge_requests/reviewer_type_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MergeRequestReviewer'] do
+ specify { expect(described_class).to require_graphql_authorizations(:read_user) }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ id
+ bot
+ user_permissions
+ snippets
+ name
+ username
+ email
+ publicEmail
+ avatarUrl
+ webUrl
+ webPath
+ todos
+ state
+ status
+ location
+ authoredMergeRequests
+ assignedMergeRequests
+ reviewRequestedMergeRequests
+ groupMemberships
+ groupCount
+ projectMemberships
+ starredProjects
+ callouts
+ merge_request_interaction
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+
+ describe '#merge_request_interaction' do
+ subject { described_class.fields['mergeRequestInteraction'] }
+
+ it 'returns the correct type' do
+ is_expected.to have_graphql_type(Types::UserMergeRequestInteractionType)
+ end
+
+ it 'has the correct arguments' do
+ is_expected.to have_attributes(arguments: be_empty)
+ end
+ end
+end
diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb
index 806495250ac..5c2ae5cea3c 100644
--- a/spec/graphql/types/milestone_type_spec.rb
+++ b/spec/graphql/types/milestone_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do
it 'has the expected fields' do
expected_fields = %w[
- id title description state web_path
+ id iid title description state web_path
due_date start_date created_at updated_at
project_milestone group_milestone subgroup_milestone
stats
diff --git a/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb b/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb
new file mode 100644
index 00000000000..18b17286654
--- /dev/null
+++ b/spec/graphql/types/packages/conan/file_metadatum_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ConanFileMetadata'] do
+ it 'includes conan file metadatum fields' do
+ expected_fields = %w[
+ id created_at updated_at recipe_revision package_revision conan_package_reference conan_file_type
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb b/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb
new file mode 100644
index 00000000000..379cb5168a8
--- /dev/null
+++ b/spec/graphql/types/packages/conan/metadatum_file_type_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ConanMetadatumFileTypeEnum'] do
+ it 'uses all possible options from model' do
+ expected_keys = ::Packages::Conan::FileMetadatum.conan_file_types
+ .keys
+ .map(&:upcase)
+
+ expect(described_class.values.keys).to contain_exactly(*expected_keys)
+ end
+end
diff --git a/spec/graphql/types/packages/conan/metadatum_type_spec.rb b/spec/graphql/types/packages/conan/metadatum_type_spec.rb
new file mode 100644
index 00000000000..f8f24ffc95a
--- /dev/null
+++ b/spec/graphql/types/packages/conan/metadatum_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['ConanMetadata'] do
+ it 'includes conan metadatum fields' do
+ expected_fields = %w[
+ id created_at updated_at package_username package_channel recipe recipe_path
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/package_without_versions_type_spec.rb b/spec/graphql/types/packages/package_details_type_spec.rb
index faa79e588d5..06093813315 100644
--- a/spec/graphql/types/packages/package_without_versions_type_spec.rb
+++ b/spec/graphql/types/packages/package_details_type_spec.rb
@@ -2,10 +2,10 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['PackageWithoutVersions'] do
+RSpec.describe GitlabSchema.types['PackageDetailsType'] do
it 'includes all the package fields' do
expected_fields = %w[
- id name version created_at updated_at package_type tags project pipelines
+ id name version created_at updated_at package_type tags project pipelines versions package_files
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/packages/package_file_type_spec.rb b/spec/graphql/types/packages/package_file_type_spec.rb
new file mode 100644
index 00000000000..8e20aea5220
--- /dev/null
+++ b/spec/graphql/types/packages/package_file_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['PackageFile'] do
+ it 'includes package file fields' do
+ expected_fields = %w[
+ id file_name created_at updated_at size file_name download_path file_md5 file_sha1 file_sha256 file_metadata
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/packages/package_type_spec.rb b/spec/graphql/types/packages/package_type_spec.rb
index 43289a019b3..544d6ddc3af 100644
--- a/spec/graphql/types/packages/package_type_spec.rb
+++ b/spec/graphql/types/packages/package_type_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['Package'] do
id name version package_type
created_at updated_at
project
- tags pipelines versions
+ tags pipelines metadata versions
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 9579ef8b99b..f2c4068f048 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -106,7 +106,8 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(secure_analyzers_prefix['type']).to eq('string')
expect(secure_analyzers_prefix['field']).to eq('SECURE_ANALYZERS_PREFIX')
expect(secure_analyzers_prefix['label']).to eq('Image prefix')
- expect(secure_analyzers_prefix['defaultValue']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
+ expect(secure_analyzers_prefix['defaultValue'])
+ .to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(secure_analyzers_prefix['value']).to eq('registry.gitlab.com/gitlab-org/security-products/analyzers')
expect(secure_analyzers_prefix['size']).to eq('LARGE')
expect(secure_analyzers_prefix['options']).to be_nil
@@ -124,8 +125,8 @@ RSpec.describe GitlabSchema.types['Project'] do
it "returns the project's sast configuration for analyzer variables" do
analyzer = subject.dig('data', 'project', 'sastCiConfiguration', 'analyzers', 'nodes').first
- expect(analyzer['name']).to eq('brakeman')
- expect(analyzer['label']).to eq('Brakeman')
+ expect(analyzer['name']).to eq('bandit')
+ expect(analyzer['label']).to eq('Bandit')
expect(analyzer['enabled']).to eq(true)
end
@@ -184,9 +185,11 @@ RSpec.describe GitlabSchema.types['Project'] do
context 'when repository is accessible only by team members' do
it "returns no configuration" do
- project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED,
- builds_access_level: ProjectFeature::DISABLED,
- repository_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED,
+ repository_access_level: ProjectFeature::PRIVATE
+ )
secure_analyzers_prefix = subject.dig('data', 'project', 'sastCiConfiguration')
expect(secure_analyzers_prefix).to be_nil
@@ -240,6 +243,7 @@ RSpec.describe GitlabSchema.types['Project'] do
:assignee_username,
:reviewer_username,
:milestone_title,
+ :not,
:sort
)
end
@@ -342,8 +346,13 @@ RSpec.describe GitlabSchema.types['Project'] do
let_it_be(:project) { create(:project, :public) }
context 'when project has Jira imports' do
- let_it_be(:jira_import1) { create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago) }
- let_it_be(:jira_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) }
+ let_it_be(:jira_import1) do
+ create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago)
+ end
+
+ let_it_be(:jira_import2) do
+ create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago)
+ end
it 'retrieves the imports' do
expect(subject).to contain_exactly(jira_import1, jira_import2)
@@ -363,4 +372,11 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Ci::AnalyticsType) }
it { is_expected.to have_graphql_resolver(Resolvers::ProjectPipelineStatisticsResolver) }
end
+
+ describe 'jobs field' do
+ subject { described_class.fields['jobs'] }
+
+ it { is_expected.to have_graphql_type(Types::Ci::JobType.connection_type) }
+ it { is_expected.to have_graphql_arguments(:statuses) }
+ end
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index cb8e875dbf4..d3dcdd260b0 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -98,6 +98,6 @@ RSpec.describe GitlabSchema.types['Query'] do
describe 'package field' do
subject { described_class.fields['package'] }
- it { is_expected.to have_graphql_type(Types::Packages::PackageType) }
+ it { is_expected.to have_graphql_type(Types::Packages::PackageDetailsType) }
end
end
diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb
new file mode 100644
index 00000000000..f8647e4e964
--- /dev/null
+++ b/spec/graphql/types/repository/blob_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Repository::BlobType do
+ specify { expect(described_class.graphql_name).to eq('RepositoryBlob') }
+
+ specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) }
+end
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index e9199bd286e..fa1e54dfcfa 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -12,4 +12,8 @@ RSpec.describe GitlabSchema.types['Repository'] do
specify { expect(described_class).to have_graphql_field(:tree) }
specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) }
+
+ specify { expect(described_class).to have_graphql_field(:blobs) }
+
+ specify { expect(described_class).to have_graphql_field(:branch_names, calls_gitaly?: true, complexity: 170) }
end
diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb
index 4d827186a9b..b87770ebe8d 100644
--- a/spec/graphql/types/snippet_type_spec.rb
+++ b/spec/graphql/types/snippet_type_spec.rb
@@ -161,6 +161,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
describe '#blobs' do
let_it_be(:snippet) { create(:personal_snippet, :public, author: user) }
+
let(:query_blobs) { subject.dig('data', 'snippets', 'nodes')[0].dig('blobs', 'nodes') }
let(:paths) { [] }
let(:query) do
@@ -201,6 +202,7 @@ RSpec.describe GitlabSchema.types['Snippet'] do
context 'when snippet has repository' do
let_it_be(:snippet) { create(:personal_snippet, :repository, :public, author: user) }
+
let(:blobs) { snippet.blobs }
it_behaves_like 'an array'
diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb
new file mode 100644
index 00000000000..38bd70d5097
--- /dev/null
+++ b/spec/graphql/types/timelog_type_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Timelog'] do
+ let(:fields) { %i[spent_at time_spent user issue note] }
+
+ it { expect(described_class.graphql_name).to eq('Timelog') }
+ it { expect(described_class).to have_graphql_fields(fields) }
+ it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) }
+
+ describe 'user field' do
+ subject { described_class.fields['user'] }
+
+ it 'returns user' do
+ is_expected.to have_non_null_graphql_type(Types::UserType)
+ end
+ end
+
+ describe 'issue field' do
+ subject { described_class.fields['issue'] }
+
+ it 'returns issue' do
+ is_expected.to have_graphql_type(Types::IssueType)
+ end
+ end
+
+ describe 'note field' do
+ subject { described_class.fields['note'] }
+
+ it 'returns note' do
+ is_expected.to have_graphql_type(Types::Notes::NoteType)
+ end
+ end
+end
diff --git a/spec/graphql/types/user_merge_request_interaction_type_spec.rb b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
new file mode 100644
index 00000000000..f424c9200ab
--- /dev/null
+++ b/spec/graphql/types/user_merge_request_interaction_type_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['UserMergeRequestInteraction'] do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:interaction) { ::Users::MergeRequestInteraction.new(user: user, merge_request: merge_request.reset) }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ can_merge
+ can_update
+ review_state
+ reviewed
+ approved
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
+ end
+
+ def resolve(field_name)
+ resolve_field(field_name, interaction, current_user: current_user)
+ end
+
+ describe '#can_merge' do
+ subject { resolve(:can_merge) }
+
+ context 'when the user cannot merge' do
+ it { is_expected.to be false }
+ end
+
+ context 'when the user can merge' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it { is_expected.to be true }
+ end
+ end
+
+ describe '#can_update' do
+ subject { resolve(:can_update) }
+
+ context 'when the user cannot update the MR' do
+ it { is_expected.to be false }
+ end
+
+ context 'when the user can update the MR' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be true }
+ end
+ end
+
+ describe '#review_state' do
+ subject { resolve(:review_state) }
+
+ context 'when the user has not been asked to review the MR' do
+ it { is_expected.to be_nil }
+
+ it 'implies not reviewed' do
+ expect(resolve(:reviewed)).to be false
+ end
+ end
+
+ context 'when the user has been asked to review the MR' do
+ before do
+ merge_request.reviewers << user
+ end
+
+ it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['UNREVIEWED'].value) }
+
+ it 'implies not reviewed' do
+ expect(resolve(:reviewed)).to be false
+ end
+ end
+
+ context 'when the user has provided a review' do
+ before do
+ merge_request.merge_request_reviewers.create!(reviewer: user, state: MergeRequestReviewer.states['reviewed'])
+ end
+
+ it { is_expected.to eq(Types::MergeRequestReviewStateEnum.values['REVIEWED'].value) }
+
+ it 'implies reviewed' do
+ expect(resolve(:reviewed)).to be true
+ end
+ end
+ end
+
+ describe '#approved' do
+ subject { resolve(:approved) }
+
+ context 'when the user has not approved the MR' do
+ it { is_expected.to be false }
+ end
+
+ context 'when the user has approved the MR' do
+ before do
+ merge_request.approved_by_users << user
+ end
+
+ it { is_expected.to be true }
+ end
+ end
+end